diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5e0151..e9f2aff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,19 @@
усыпляет; медиаплееры запрашивают удержание экрана сами, так что фильмы не прерываются.
По умолчанию `false` — поведение существующих установок не меняется, пока поле не добавят руками.
+### Исправлено
+- **Порог заряда в панели и меню больше не расходится с железом.** Если порог менял кто-то снаружи
+ (Xiaomi PC Manager, чужая утилита), быстрая панель продолжала показывать старое значение — она
+ рисовала из конфига и прошивку не перечитывала вовсе. Теперь порог подтягивается из прошивки в момент
+ показа панели и пересборки меню трея (`AppController.SyncCareFromFirmware()`): фонового опроса не
+ добавилось, читаем только когда UI открывают. Принимается только валидный порог < 100 %: показание
+ «100» неотличимо от транзиента после сна/смены питания (EC теряет лимит до ре-арма `ChargeGuard`) и
+ не принимается — иначе панель, открытая сразу после resume, молча выключала бы защиту. Прошивка не
+ ответила → остаётся значение конфига; при активном «В дорогу» примирение не выполняется. Заодно
+ исправлена та же ошибка в меню трея: галочка считалась от живого значения, а процент в подписи брался
+ из конфига, из-за чего меню могло показать «беречь 60 %» при 50 % в железе.
+ Разбор — [docs/12-charge-levels.md](docs/12-charge-levels.md) (XIC-17).
+
## [0.8.0] — 2026-07-29
### Добавлено
diff --git a/README.md b/README.md
index 8dfdcb6..014d07c 100644
--- a/README.md
+++ b/README.md
@@ -58,8 +58,8 @@ double-click; current direction is shown by color (charging green / discharging
- 🔌 **Charger wattage** — when the charger is plugged in, show the connected PD adapter's
wattage (watts) in the OSD and in the Monitor. Over the charge icon — a **PSU quality badge**:
🔴 "!" if the adapter is weaker than the configured threshold (slow charging), ⚪ "?" if the
- PSU is non-PD (e.g. plain 5 V — wattage can't be negotiated). The icon still shows the 80/100
- limit. The threshold is configurable (Settings → Battery). Driver-free (read-only).
+ PSU is non-PD (e.g. plain 5 V — wattage can't be negotiated). The icon still shows the current
+ charge limit. The threshold is configurable (Settings → Battery). Driver-free (read-only).
- 🩺 **Battery health** — Settings → Battery: actual wear (current vs. design capacity),
charge cycle count, capacity in Wh. Stock ACPI/Windows data, read-only.
- ⚡ **Performance modes**: Eco (hidden firmware mode) / Quiet / Auto /
@@ -70,8 +70,8 @@ double-click; current direction is shown by color (charging green / discharging
- microphone on/off, keyboard backlight (off / 50% / 100% / auto).
- 🅼 **Mi button**:
- short press — cycle through modes with an OSD (configurable);
- - double click — charge 80/100 (configurable);
- - hold — quick settings panel (modes + charge 80/100, closes on Esc/X/click-outside).
+ - double click — toggle the charge limit (configurable);
+ - hold — quick settings panel (modes + charge limit, closes on Esc/X/click-outside).
- ⌨️ **Reviving "dead" keys** with remapping: the Mi clicks and the "settings" / AI /
"projection" keys can be bound to any function — from cycling modes to launching your
own program (see "Key remapping"); the microphone key mutes the system mic, the backlight
@@ -222,7 +222,7 @@ menu item — the utility lifts the limit once and tops up to 100%.
- Manually picking the threshold/100 pill also cancels the mode. With a permanent "100%" the button is
inactive (nothing to top up).
-The 80/100 pills show the **base** setting — "travel" is a temporary override on top of it
+The "threshold/100" pills show the **base** setting — "travel" is a temporary override on top of it
(driver-free, the same charge WMI channel). In `config.json`: `"TravelMode"`, `"TravelSound"`.
A custom ready sound — the "Custom sound file" field in the same settings (or `config.json`;
@@ -333,7 +333,7 @@ When editing `AcRefreshRate`/`BatteryRefreshRate` directly in the config, restar
Each key gets its own action: **Settings → Keys**. The slots are the single and double Mi-button
click, the "Settings" (gear), AI and "Projection" keys. Any slot can be bound to: cycle modes,
-charge 80/100, quick panel, owl mode, Monitor, "travel", touchpad and touchscreen on/off, the
+charge limit on/off, quick panel, owl mode, Monitor, "travel", touchpad and touchscreen on/off, the
system "Projection (Win+P)" / "Windows Settings" / "Copilot (Win+C)", launching your own program,
or "Nothing".
diff --git a/README.ru.md b/README.ru.md
index 293d905..7345c41 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -59,7 +59,7 @@ RAM, **температура** «горячей точки» (горячая з
- 🔌 **Мощность зарядника** — при подключении зарядки показываем мощность подключённого
PD-адаптера (ватты) в OSD и в «Мониторе». Поверх иконки заряда — **бейдж качества блока**:
🔴 «!» если адаптер слабее заданного порога (медленный заряд), ⚪ «?» если блок не-PD (например
- обычный 5 В — мощность не согласуется). При этом иконка по-прежнему показывает лимит 80/100.
+ обычный 5 В — мощность не согласуется). При этом иконка по-прежнему показывает текущий лимит заряда.
Порог настраивается (Настройки → Батарея). Driver-free (только чтение).
- 🩺 **Здоровье батареи** — Настройки → Батарея: реальный износ (текущая ёмкость к проектной),
число циклов заряда, ёмкость в Вт·ч. Штатные данные ACPI/Windows, только чтение.
@@ -71,8 +71,8 @@ RAM, **температура** «горячей точки» (горячая з
- микрофон вкл/выкл, подсветка клавиатуры (выкл / 50% / 100% / авто).
- 🅼 **Mi-кнопка**:
- короткое нажатие — циклическое переключение режимов с OSD (настраивается);
- - двойной клик — заряд 80/100 (настраивается);
- - удержание — панель быстрых настроек (режимы + заряд 80/100, закрытие по Esc/X/клику вне).
+ - двойной клик — переключение лимита заряда (настраивается);
+ - удержание — панель быстрых настроек (режимы + лимит заряда, закрытие по Esc/X/клику вне).
- ⌨️ **Оживление «мёртвых» клавиш** с переназначением: на клики Mi и клавиши
«настройки» / AI / «проекция» вешается любая функция — от цикла режимов до запуска
своей программы (см. «Переназначение клавиш»); клавиша микрофона мьютит системный
@@ -225,7 +225,7 @@ dotnet publish src/XiControl.csproj -c Release -r win-x64 --self-contained -p:Pu
- Ручной выбор пилюли «порог/100» тоже отменяет режим. При постоянном «100%» кнопка неактивна
(дозаряжать некуда).
-Пилюли 80/100 показывают **базовую** настройку — «В дорогу» это временный оверрайд поверх неё
+Пилюли «порог/100» показывают **базовую** настройку — «В дорогу» это временный оверрайд поверх неё
(driver-free, тот же WMI-канал заряда). В `config.json`: `"TravelMode"`, `"TravelSound"`.
Свой звук готовности — поле «Свой звуковой файл» там же в настройках (или `config.json`;
@@ -335,7 +335,7 @@ dotnet publish src/XiControl.csproj -c Release -r win-x64 --self-contained -p:Pu
Каждой клавише — своё действие: **Настройки → Клавиши**. Слоты — одиночный и двойной
клик Mi-кнопки, клавиши «Настройки» (шестерёнка), AI и «Проекция». На любой слот можно
-навесить: цикл режимов, заряд 80/100, быструю панель, режим совы, «Монитор»,
+навесить: цикл режимов, лимит заряда вкл/выкл, быструю панель, режим совы, «Монитор»,
«В дорогу», тачпад и сенсорный экран вкл/выкл, системные «Проекция (Win+P)» /
«Параметры Windows» / «Copilot (Win+C)», запуск своей программы или «Ничего».
diff --git a/docs/12-charge-levels.md b/docs/12-charge-levels.md
index 70f9006..01dbcc3 100644
--- a/docs/12-charge-levels.md
+++ b/docs/12-charge-levels.md
@@ -87,6 +87,37 @@ OEM (`Xiaomi PC Manager`) даёт селектор лимита заряда (4
«В дорогу»; иначе просто запоминает. `ChargeGuard` армит **выбранный** порог.
- UI: панель/меню/OSD показывают реальный %, пикер — Настройки → Батарея.
+## Примирение с внешней сменой (XIC-17)
+
+Канала уведомления в MIFS нет, поэтому «на лету» две программы не синхронизируются (см. выше). Но
+подхватить чужое изменение **в момент показа UI** можно — и нужно, иначе внешняя смена до нас не
+доходит вовсе. `AppController.SyncCareFromFirmware()` читает `GET 0x10/02` и приводит конфиг к
+прошивке; зовётся из `QuickPanelForm.RefreshState()` (до `Show()`, рядом с чтением режима и тачпада) и
+из `TrayMenuBuilder` при пересборке меню. Фонового опроса не появилось — только когда UI открывают.
+
+Тонкости, без которых это ломается:
+
+- **Прошивка молчит** (`null`) → конфиг не трогаем: лучше показать своё значение, чем затереть нулём.
+- **Принимаем только валидный порог < 100** (`ChargeCodeForPercent != null`): другой уровень EC сам
+ не породит — это всегда чей-то осознанный SET, его принимать безопасно.
+- **«100 %» НЕ принимаем — это защита от гонки с ChargeGuard.** EC читается как 100 и когда защиту
+ выключили снаружи, и — транзиентно — после сна/смены питания: лимит на этих событиях теряется, а
+ гард переармирует его с дебаунсом 1.5 с. Отличить эти случаи нельзя. Приняв 100, мы сбросили бы
+ `ChargeCare` в конфиге — и гард, который читает желаемый порог из конфига, разоружился бы навсегда:
+ панель, открытая сразу после resume или выдёргивания зарядника, молча убивала бы защиту. Внешнее
+ «выключить» гард всё равно перебивает на следующем событии питания (документированное поведение),
+ поэтому UI показывает намерение пользователя, а не мгновенное состояние EC.
+- **При активном «В дорогу» примирение не выполняется**: там прошивка намеренно держит 100 % при
+ `ChargeCare = true`, и примирение сбросило бы флаг, сломав режим.
+- **`CareChanged` не дёргаем.** Это не действие пользователя, а подхват чужого; при скрытой панели
+ `TrayApp` показывает на это событие OSD — всплывашка вылетала бы на каждое открытие панели.
+- Конфиг пишем только при фактическом отличии, факт принятия — в лог.
+
+Заодно исправлена та же ошибка в меню трея: галочка считалась от живого значения, а процент в подписи
+брался из конфига, поэтому меню могло показать «беречь 60 %» с галочкой при 50 % в железе. Теперь и
+панель, и меню рисуют из конфига — он уже приведён к прошивке. Тултип трея сознательно оставлен на
+конфиге: он живёт на 30-секундном опросе, и правдивость потребовала бы фонового WMI-вызова.
+
**Деградация на моделях без granular** сделана рантаймом, а не гейтом по SKU (в духе «набор функций
определяем в рантайме»): прошивка сама валидирует уровень, отказ (`OUT[1] != 0x80`) → `SetChargeLimit`
возвращает `false` → выбор откатывается, пользователь получает честный error-OSD, а 80/100 (legacy-коды
diff --git a/src/Localization/lang/en.json b/src/Localization/lang/en.json
index 24bbf0d..7ad1757 100644
--- a/src/Localization/lang/en.json
+++ b/src/Localization/lang/en.json
@@ -106,7 +106,7 @@
"settings.key.proj.desc": "The display-switch key.",
"settings.key.command": "Program / file / URL",
"settings.act.modes": "Cycle modes",
- "settings.act.charge": "Charge 80 / 100 %",
+ "settings.act.charge": "Charge limit on/off",
"settings.act.panel": "Quick panel",
"settings.act.owl": "Owl mode",
"settings.act.monitor": "Monitor",
@@ -143,7 +143,7 @@
"settings.api.cmd.mode": "Performance modes",
"settings.api.cmd.mode.desc": "POST /mode — switch the mode.",
"settings.api.cmd.care": "Battery protection",
- "settings.api.cmd.care.desc": "POST /care — toggle “protect ~80%”.",
+ "settings.api.cmd.care.desc": "POST /care — toggle the charge limit.",
"settings.api.cmd.travel": "Travel mode",
"settings.api.cmd.travel.desc": "POST /travel — one-off charge to 100%.",
"settings.api.cmd.owl": "Owl mode",
diff --git a/src/Localization/lang/ru.json b/src/Localization/lang/ru.json
index 585c3a6..3a31dff 100644
--- a/src/Localization/lang/ru.json
+++ b/src/Localization/lang/ru.json
@@ -99,14 +99,14 @@
"settings.key.mi.double.desc": "«Ничего» — жест отключён, одиночный клик срабатывает мгновенно.",
"settings.keys.other": "Прочие клавиши",
"settings.key.settings": "Клавиша «Настройки»",
- "settings.key.settings.desc": "Клавиша с шестерёнкой. При открытой панели — всегда заряд 80/100.",
+ "settings.key.settings.desc": "Клавиша с шестерёнкой. При открытой панели — всегда переключает лимит заряда.",
"settings.key.ai": "AI-клавиша",
"settings.key.ai.desc": "Клавиша нейропомощника.",
"settings.key.proj": "Клавиша «Проекция»",
"settings.key.proj.desc": "Клавиша переключения экранов.",
"settings.key.command": "Программа / файл / URL",
"settings.act.modes": "Цикл режимов",
- "settings.act.charge": "Заряд 80 / 100 %",
+ "settings.act.charge": "Лимит заряда вкл/выкл",
"settings.act.panel": "Быстрая панель",
"settings.act.owl": "Режим совы",
"settings.act.monitor": "Монитор",
@@ -143,7 +143,7 @@
"settings.api.cmd.mode": "Режимы производительности",
"settings.api.cmd.mode.desc": "POST /mode — переключение режима.",
"settings.api.cmd.care": "Защита заряда",
- "settings.api.cmd.care.desc": "POST /care — «беречь ~80%» вкл/выкл.",
+ "settings.api.cmd.care.desc": "POST /care — лимит заряда вкл/выкл.",
"settings.api.cmd.travel": "Режим «В дорогу»",
"settings.api.cmd.travel.desc": "POST /travel — разовый заряд до 100%.",
"settings.api.cmd.owl": "Режим совы",
diff --git a/src/Localization/lang/zh.json b/src/Localization/lang/zh.json
index 8c88614..adcd00e 100644
--- a/src/Localization/lang/zh.json
+++ b/src/Localization/lang/zh.json
@@ -106,7 +106,7 @@
"settings.key.proj.desc": "屏幕切换键。",
"settings.key.command": "程序 / 文件 / URL",
"settings.act.modes": "循环性能模式",
- "settings.act.charge": "充电 80 / 100 %",
+ "settings.act.charge": "充电限制开关",
"settings.act.panel": "快速面板",
"settings.act.owl": "猫头鹰模式",
"settings.act.monitor": "监视器",
@@ -143,7 +143,7 @@
"settings.api.cmd.mode": "性能模式",
"settings.api.cmd.mode.desc": "POST /mode — 切换性能模式。",
"settings.api.cmd.care": "电池保护",
- "settings.api.cmd.care.desc": "POST /care — 开关“保持约 80%”。",
+ "settings.api.cmd.care.desc": "POST /care — 开关充电限制。",
"settings.api.cmd.travel": "出行模式",
"settings.api.cmd.travel.desc": "POST /travel — 一次性充电至 100%。",
"settings.api.cmd.owl": "猫头鹰模式",
diff --git a/src/Ui/AppController.cs b/src/Ui/AppController.cs
index a6e0eab..d965dc0 100644
--- a/src/Ui/AppController.cs
+++ b/src/Ui/AppController.cs
@@ -153,6 +153,38 @@ public void ToggleCharge()
ToggleCare(cur >= 100); // сейчас 100 → включить «беречь X%»; иначе → 100
}
+ ///
+ /// Принять порог заряда из прошивки: внешнюю смену (Xiaomi PC Manager, чужая утилита) мы иначе
+ /// не заметим — канала уведомления в MIFS нет. Зовётся в момент показа панели и пересборки меню
+ /// трея, поэтому фонового опроса не появляется.
+ /// Принимаем ТОЛЬКО валидный порог < 100: другой уровень EC сам не породит — это всегда
+ /// чей-то осознанный SET. А вот «100%» неотличим от транзиента: EC теряет лимит после сна/смены
+ /// питания, и до ре-арма ChargeGuard (дебаунс 1.5 с) читается как 100. Принять его = сбросить
+ /// ChargeCare в конфиге и разоружить гард навсегда (панель, открытая сразу после resume/выдёргивания
+ /// зарядника, молча убивала бы защиту). Внешнее «выключить» гард всё равно перебивает на следующем
+ /// событии питания — это документированное поведение, поэтому «100» честно игнорируем.
+ /// здесь намеренно НЕ дёргаем: это не действие пользователя, а
+ /// подхват чужого. При скрытой панели TrayApp показывает на это событие OSD — то есть всплывашка
+ /// вылетала бы на каждое открытие панели. Панель и меню и так рисуются сразу после вызова.
+ ///
+ /// true — значение приняли, конфиг изменился.
+ public bool SyncCareFromFirmware()
+ {
+ // «В дорогу» намеренно держит 100% при включённой защите — это наше состояние, не внешнее
+ if (_cfg.TravelMode) return false;
+ if (Safe(() => _mifs.GetChargeLimit(), null) is not int live) return false;
+ if (live >= 100 || Mifs.ChargeCodeForPercent(live) is null) return false;
+
+ bool changed = false;
+ if (!_cfg.ChargeCare) { _cfg.ChargeCare = true; changed = true; }
+ if (live != _cfg.CareLimitPercent) { _cfg.CareLimitPercent = live; changed = true; }
+ if (!changed) return false;
+
+ Log.Write($"Заряд: принят порог прошивки — беречь {live}%");
+ _cfg.Save();
+ return true;
+ }
+
/// Установить «беречь батарею» (порог X% из настроек) либо 100%. Ручная смена отменяет
/// «В дорогу». Прошивка не приняла → конфиг не трогаем (реальное состояние не изменилось) и честно
/// сообщаем об ошибке вместо оптимистичного «успеха» (Фаза 6.2).
diff --git a/src/Ui/QuickPanelForm.cs b/src/Ui/QuickPanelForm.cs
index 421eafc..3075017 100644
--- a/src/Ui/QuickPanelForm.cs
+++ b/src/Ui/QuickPanelForm.cs
@@ -61,6 +61,10 @@ public sealed class QuickPanelForm : FlyoutForm
public Action? ToggleTouchpad;
public Action? ToggleTouchscreen;
+ /// Перед показом: подтянуть порог заряда из прошивки в конфиг (его мог сменить кто-то
+ /// снаружи). Запись в конфиг — дело контроллера, панель только просит (XIC-17).
+ public Action? SyncCare;
+
/// Кнопка-график слева от крестика: открыть окно «Монитор» (владелец — трей).
public Action? MonitorRequested;
@@ -110,6 +114,10 @@ private void RefreshState()
// деградация с логом: панель откроется и без ответивших подсистем
try { _mode = _mifs.GetPerfMode(); }
catch (Exception ex) { Log.Ex("QuickPanel.Mode", ex); _mode = null; }
+ // порог заряда рисуем из конфига, но сперва приводим его к прошивке: иначе внешняя смена
+ // до панели не доходила вовсе, и подпись расходилась с поведением клика (XIC-17)
+ try { SyncCare?.Invoke(); }
+ catch (Exception ex) { Log.Ex("QuickPanel.SyncCare", ex); }
try { _tpAvail = _cfg.TouchpadFeature && _tp.Available; _tpOn = _tp.IsEnabled() ?? false; }
catch (Exception ex) { Log.Ex("QuickPanel.Touchpad", ex); _tpAvail = false; }
try { _tsAvail = _cfg.TouchscreenFeature && _ts.Available; _tsOn = _ts.IsEnabled() ?? false; }
diff --git a/src/Ui/TrayApp.cs b/src/Ui/TrayApp.cs
index 40ad572..17ae2cb 100644
--- a/src/Ui/TrayApp.cs
+++ b/src/Ui/TrayApp.cs
@@ -114,6 +114,7 @@ public TrayApp(IMifsClient mifs, AppConfig cfg, IKeyEventSource events,
_panel.SetAutoHz = _controller.ToggleAutoHz;
_panel.ToggleTouchpad = _controller.ToggleTouchpad;
_panel.ToggleTouchscreen = _controller.ToggleTouchscreen;
+ _panel.SyncCare = () => _controller.SyncCareFromFirmware();
// Уведомления контроллера → обратная связь UI: панель открыта — обновляется она,
// иначе OSD; значок обновляем после смены режима. Сама логика — в AppController.
diff --git a/src/Ui/TrayMenuBuilder.cs b/src/Ui/TrayMenuBuilder.cs
index 9fe4d08..f2d50af 100644
--- a/src/Ui/TrayMenuBuilder.cs
+++ b/src/Ui/TrayMenuBuilder.cs
@@ -80,9 +80,11 @@ private void Build()
Menu.ImageScalingSize = new Size(imgSz, imgSz);
// --- Заряд ---
- int lim = Safe(() => _mifs.GetChargeLimit(), _cfg.ChargeCare ? _cfg.CarePercent() : 100) ?? 100;
- bool care = lim < 100; // «беречь» включено, когда порог не 100%
- var charge = new ToolStripMenuItem(Loc.T("menu.charge", _cfg.CarePercent())) { Checked = care };
+ // Порог приводим к прошивке (её мог сменить кто-то снаружи) и дальше рисуем из конфига: раньше
+ // галочка считалась от живого значения, а процент в подписи брался из конфига — меню могло
+ // показать «беречь 60%» с галочкой, когда в железе 50% (XIC-17).
+ _controller.SyncCareFromFirmware();
+ var charge = new ToolStripMenuItem(Loc.T("menu.charge", _cfg.CarePercent())) { Checked = _cfg.ChargeCare };
// состояние читаем в момент клика: пока меню висело, его мог сменить ChargeGuard
charge.Click += (_, _) => _controller.ToggleCharge();
Menu.Items.Add(charge);
diff --git a/tests/XiControl.Tests/AppControllerTests.cs b/tests/XiControl.Tests/AppControllerTests.cs
index 31899b5..b4402ec 100644
--- a/tests/XiControl.Tests/AppControllerTests.cs
+++ b/tests/XiControl.Tests/AppControllerTests.cs
@@ -156,6 +156,93 @@ public void SetCareLimit_FirmwareRejects_KeepsPreviousChoice()
failed.Should().BeTrue();
}
+ // ---- Примирение порога с прошивкой (XIC-17) ----
+
+ [Fact]
+ public void SyncCare_AdoptsExternalLimit_Silently()
+ {
+ _cfg.ChargeCare = true;
+ _mifs.ChargeLimit = 50; // порог сменили снаружи (Xiaomi PC Manager)
+
+ _c.SyncCareFromFirmware().Should().BeTrue();
+
+ _cfg.CareLimitPercent.Should().Be(50, "источник истины — прошивка (docs/12)");
+ _cfg.ChargeCare.Should().BeTrue();
+ _events.Should().BeEmpty("подхват чужого значения — не действие пользователя: OSD не показываем");
+ }
+
+ [Fact]
+ public void SyncCare_Reads100_DoesNotDisarm()
+ {
+ // EC читается как 100 и когда защиту выключили снаружи, и — транзиентно — после сна/смены
+ // питания, пока ChargeGuard не переармил (дебаунс). Принять 100 = сбросить ChargeCare и
+ // разоружить гард навсегда: панель, открытая сразу после resume, молча убивала бы защиту.
+ _cfg.ChargeCare = true;
+ _mifs.ChargeLimit = 100;
+
+ _c.SyncCareFromFirmware().Should().BeFalse();
+
+ _cfg.ChargeCare.Should().BeTrue("«100» неотличим от транзиента до ре-арма — не принимаем");
+ _cfg.CareLimitPercent.Should().Be(80);
+ }
+
+ [Fact]
+ public void SyncCare_ExternalArming_IsAdopted()
+ {
+ _cfg.ChargeCare = false;
+ _mifs.ChargeLimit = 60; // защиту включили снаружи: EC сам уровень не породит — это чей-то SET
+
+ _c.SyncCareFromFirmware().Should().BeTrue();
+
+ _cfg.ChargeCare.Should().BeTrue();
+ _cfg.CareLimitPercent.Should().Be(60);
+ }
+
+ [Fact]
+ public void SyncCare_FirmwareSilent_KeepsConfig()
+ {
+ _cfg.ChargeCare = true;
+ _mifs.ChargeLimit = null; // прошивка не ответила
+
+ _c.SyncCareFromFirmware().Should().BeFalse();
+
+ _cfg.ChargeCare.Should().BeTrue("лучше показать своё значение, чем затереть его нулём");
+ _cfg.CareLimitPercent.Should().Be(80);
+ }
+
+ [Fact]
+ public void SyncCare_DuringTravel_DoesNothing()
+ {
+ _cfg.ChargeCare = true;
+ _cfg.TravelMode = true;
+ _mifs.ChargeLimit = 100; // «в дорогу» и держит 100% — это наше состояние, не внешнее
+
+ _c.SyncCareFromFirmware().Should().BeFalse();
+
+ _cfg.ChargeCare.Should().BeTrue("иначе примирение сломало бы «в дорогу»");
+ _cfg.TravelMode.Should().BeTrue();
+ }
+
+ [Fact]
+ public void SyncCare_UnknownPercent_KeepsChoice()
+ {
+ _cfg.ChargeCare = true;
+ _mifs.ChargeLimit = 90; // такого уровня в наборе нет (docs/12)
+
+ _c.SyncCareFromFirmware().Should().BeFalse();
+
+ _cfg.CareLimitPercent.Should().Be(80, "неизвестный уровень в конфиг не принимаем");
+ }
+
+ [Fact]
+ public void SyncCare_AlreadyInSync_ReportsNoChange()
+ {
+ _cfg.ChargeCare = true;
+ _mifs.ChargeLimit = 80; // ровно то, что в конфиге
+
+ _c.SyncCareFromFirmware().Should().BeFalse("без отличий не пишем конфиг на каждое открытие панели");
+ }
+
[Fact]
public void DisableTravel_IsSilentReset()
{