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() {