Skip to content

Commit f05482a

Browse files
author
Maksym Shtembuliak
committed
feat: add SmartClient analysis and refine support diagnostics
Add a bounded read-only SmartClient/ShineMonitor source, isolated native authentication, exact-device evidence, source selection, translations and user documentation. Preserve existing active-learning routes. Clarify shared collector metadata and Modbus exception 03 guidance. Cover source isolation, passive acquisition and real Home Assistant options flow. Refs #23, #26, #41. No release.
1 parent 002987b commit f05482a

29 files changed

Lines changed: 1573 additions & 48 deletions

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ the GitHub release body should be rendered from the matching version section her
99

1010
### Added
1111

12+
- Added a separate **SmartClient / ShineMonitor** source for read-only device
13+
analysis. It collects exact-identity cloud readings, setting descriptions,
14+
available daily history and raw-packet fingerprints without changing the collector
15+
endpoint or sending control commands. Existing SmartESS/DESSMonitor defaults
16+
and active learning are unchanged; this source does not yet offer active
17+
verification or a built-in map for previously unsupported PV inverters.
1218
- Added protocol-level fallback profiles for unknown SMG models reporting
1319
protocol 1, 2, or 11. Protocols 1 and 11 expose 30 compatible basic controls;
1420
the July 2024 GM6200 protocol-2 document supplies a separate 54-capability
@@ -68,6 +74,15 @@ the GitHub release body should be rendered from the matching version section her
6874

6975
### Fixed
7076

77+
- Collector protocol hints no longer label SmartClient/SmartValue devices as
78+
SmartESS merely because they expose the same metadata fields. Cloud source
79+
selection remains explicit and independent of those hints.
80+
- Device-learning credentials preserve intentional password whitespace before
81+
cloud authentication.
82+
- Modbus exception `03` no longer claims that a rejected write was necessarily
83+
outside the inverter's allowed setting range. Diagnostics distinguish the
84+
profile's UI bounds from the device's unexplained rejection; write validation
85+
and confirmation behavior are unchanged.
7186
- Schema-driven Modbus polling now reports a failed cycle when every runtime
7287
block fails, instead of publishing an empty successful snapshot. Cached
7388
settings cannot hide the failure; valid partial telemetry remains available,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""EyeBond's shared signature codec; callers own endpoint and action policy."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
7+
8+
def password_signature(salt: str, password: str, action: str) -> str:
9+
"""Sign the caller's exact login suffix without choosing an auth profile."""
10+
11+
password_hash = hashlib.sha1(password.encode("utf-8")).hexdigest()
12+
return hashlib.sha1((salt + password_hash + action).encode("utf-8")).hexdigest()
13+
14+
15+
def session_signature(salt: str, secret: str, token: str, action: str) -> str:
16+
"""Sign an exact suffix, including the caller's source/client metadata."""
17+
18+
return hashlib.sha1((salt + secret + token + action).encode("utf-8")).hexdigest()

‎custom_components/eybond_local/dessmonitor_cloud.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from urllib.parse import quote
2323
from urllib.request import Request, urlopen
2424

25+
from .cloud_signing import password_signature, session_signature
2526
from .collector_identity import pn_is_same_identity
2627

2728

@@ -451,7 +452,7 @@ def build_login_url(
451452
_required_token(password, "dessmonitor_password_invalid")
452453
action = _login_action(username=username, company_key=company_key)
453454
salt = _salt_millis()
454-
sign = _sha1_lower(salt + _sha1_lower(password) + action)
455+
sign = password_signature(salt, password, action)
455456
return f"{_normalized_base_url(base_url)}?sign={sign}&salt={salt}{action}"
456457

457458

@@ -534,7 +535,7 @@ def build_signed_action_url(
534535
+ f"&_app_version_={quote(app_version, safe='')}"
535536
)
536537
salt = _salt_millis()
537-
sign = _sha1_lower(salt + session.secret + session.token + suffix)
538+
sign = session_signature(salt, session.secret, session.token, suffix)
538539
return (
539540
f"{_normalized_base_url(base_url)}?sign={sign}&salt={salt}"
540541
f"&token={quote(session.token, safe='')}{suffix}"

‎custom_components/eybond_local/drivers/modbus_write_error.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,21 @@ def _user_error_from_exception_code(
122122
native_minimum = capability.native_minimum
123123
native_maximum = capability.native_maximum
124124
if native_minimum is not None and native_maximum is not None:
125-
allowed_range = f"Allowed profile range: {native_minimum} to {native_maximum}."
125+
allowed_range = f"Profile UI range: {native_minimum} to {native_maximum}."
126126
elif native_minimum is not None:
127-
allowed_range = f"Allowed profile minimum: {native_minimum}."
127+
allowed_range = f"Profile UI minimum: {native_minimum}."
128128
elif native_maximum is not None:
129-
allowed_range = f"Allowed profile maximum: {native_maximum}."
129+
allowed_range = f"Profile UI maximum: {native_maximum}."
130130
else:
131-
allowed_range = "The inverter may enforce a narrower range than the current profile metadata."
131+
allowed_range = ""
132132

133133
return ValueError(
134134
f"illegal_data_value:{capability.key}:"
135-
f"The inverter rejected {capability.display_name!r} as out of range. "
136-
f"{allowed_range}"
135+
f"The inverter rejected the write request for {capability.display_name!r} "
136+
"(Modbus exception 03: illegal data value). This response does not "
137+
"identify an allowed setting range; the request format or a "
138+
"device-specific restriction may need checking. "
139+
f"{allowed_range}".rstrip()
137140
)
138141

139142

‎custom_components/eybond_local/flow_translations/en.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"cloud_learning_source_intro": "Choose the compatible cloud service Home Assistant should use for this check.",
2121
"cloud_learning_source_smartess": "SmartESS API",
2222
"cloud_learning_source_dessmonitor": "DESSMonitor API",
23+
"cloud_learning_source_smartclient": "SmartClient / ShineMonitor",
2324
"cloud_learning_source_valuecloud": "ValueCloud API",
2425
"cloud_learning_metadata_done": "The read-only device analysis finished.",
2526
"local_register_observation_available": "Optionally observe local readings for about {minutes} minutes in the background. This only gathers review evidence and adds no entities or controls.",
@@ -388,13 +389,13 @@
388389
"support_workflow_pending_step_2": "Send the ZIP file to the developer.",
389390
"support_workflow_pending_step_3": "Only move on to local drafts after the developer reviews the evidence.",
390391
"support_workflow_pending_advanced_hint": "Treat local drafts as a second-stage tool here. First confirm connectivity or send the support evidence.",
391-
"support_workflow_smartess_pending_level_label": "SmartESS collector evidence",
392-
"support_workflow_smartess_pending_summary": "The collector exposed SmartESS metadata, but no local inverter driver is matched yet.",
393-
"support_workflow_smartess_pending_next_action": "Create a support archive and send the ZIP file to the developer. SmartESS app support can still rely on a separate cloud-normalized identity even when the local descriptor is generic, missing, or incomplete.",
392+
"support_workflow_smartess_pending_level_label": "Collector protocol metadata",
393+
"support_workflow_smartess_pending_summary": "The collector returned protocol metadata, but no local inverter driver is matched yet.",
394+
"support_workflow_smartess_pending_next_action": "Create a support archive and send the ZIP file to the developer. The manufacturer's app may identify this inverter through its cloud even when the local descriptor is incomplete.",
394395
"support_workflow_smartess_pending_step_1": "Create a support archive.",
395396
"support_workflow_smartess_pending_step_2": "Send the ZIP file to the developer.",
396-
"support_workflow_smartess_pending_step_3": "Treat the local SmartESS descriptor as collector evidence only until a built-in mapping is confirmed.",
397-
"support_workflow_smartess_pending_advanced_hint": "Do not treat a generic or missing local SmartESS descriptor as proof that the inverter is unsupported in the SmartESS app.",
397+
"support_workflow_smartess_pending_step_3": "Treat the local protocol descriptor as collector evidence only until a built-in mapping is confirmed.",
398+
"support_workflow_smartess_pending_advanced_hint": "Protocol metadata alone does not identify the cloud app or prove a local register map. Select the app you actually use when collecting cloud evidence.",
398399
"support_workflow_family_fallback_level_label": "Read-only unverified SMG family",
399400
"support_workflow_family_fallback_summary": "This inverter is using the generic SMG family fallback. Built-in writes are intentionally disabled until the exact model is verified.",
400401
"support_workflow_family_fallback_next_action": "Create a support archive and send the ZIP file to the developer. This will help confirm the exact SMG-family model and move it beyond the read-only fallback.",

‎custom_components/eybond_local/flow_translations/ru.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"cloud_learning_source_intro": "Выберите совместимый облачный сервис для этой проверки.",
2121
"cloud_learning_source_smartess": "SmartESS API",
2222
"cloud_learning_source_dessmonitor": "DESSMonitor API",
23+
"cloud_learning_source_smartclient": "SmartClient / ShineMonitor",
2324
"cloud_learning_source_valuecloud": "ValueCloud API",
2425
"cloud_learning_metadata_done": "Анализ устройства только для чтения завершён.",
2526
"local_register_observation_available": "При желании можно около {minutes} минут собирать локальные показания в фоне. Это только данные для проверки: сущности и элементы управления не добавляются.",
@@ -388,13 +389,13 @@
388389
"support_workflow_pending_step_2": "Отправьте ZIP-файл разработчику.",
389390
"support_workflow_pending_step_3": "Переходите к локальным черновикам только после того, как разработчик просмотрит собранные данные.",
390391
"support_workflow_pending_advanced_hint": "Рассматривайте локальные черновики здесь как инструмент второго этапа. Сначала подтвердите соединение или отправьте подтверждающие данные.",
391-
"support_workflow_smartess_pending_level_label": "Есть признаки SmartESS",
392-
"support_workflow_smartess_pending_summary": "Коллектор вернул метаданные SmartESS, но локальный драйвер инвертора пока не сопоставлен.",
393-
"support_workflow_smartess_pending_next_action": "Создайте архив поддержки и отправьте ZIP-файл разработчику. Поддержка в приложении SmartESS всё ещё возможна через отдельное облачное сопоставление устройства, даже если локальный идентификатор общий, пустой или неполный.",
392+
"support_workflow_smartess_pending_level_label": "Есть данные протокола коллектора",
393+
"support_workflow_smartess_pending_summary": "Коллектор вернул данные протокола, но локальный драйвер инвертора пока не определён.",
394+
"support_workflow_smartess_pending_next_action": "Создайте архив поддержки и отправьте ZIP-файл разработчику. Приложение производителя может определять этот инвертор через облако, даже если локальные данные неполные.",
394395
"support_workflow_smartess_pending_step_1": "Создайте архив поддержки.",
395396
"support_workflow_smartess_pending_step_2": "Отправьте ZIP-файл разработчику.",
396-
"support_workflow_smartess_pending_step_3": "Считайте локальный идентификатор SmartESS только данными коллектора, пока не будет подтверждено встроенное сопоставление.",
397-
"support_workflow_smartess_pending_advanced_hint": "Не считайте общий или пустой локальный идентификатор SmartESS доказательством того, что устройство не поддерживается приложением SmartESS.",
397+
"support_workflow_smartess_pending_step_3": "Данные протокола коллектора ещё не подтверждают поддержку инвертора.",
398+
"support_workflow_smartess_pending_advanced_hint": "Эти данные не определяют облачное приложение или локальную карту регистров. Для сбора облачных данных выбирайте приложение, которым вы пользуетесь.",
398399
"support_workflow_family_fallback_level_label": "Непроверенное read-only устройство семейства SMG",
399400
"support_workflow_family_fallback_summary": "Для этого инвертора используется общий fallback-профиль семейства SMG. Встроенные записи намеренно отключены, пока точная модель не подтверждена.",
400401
"support_workflow_family_fallback_next_action": "Создайте архив поддержки и отправьте ZIP-файл разработчику. Это поможет подтвердить точную модель семейства SMG и вывести устройство из read-only fallback-режима.",

‎custom_components/eybond_local/flow_translations/uk.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"cloud_learning_source_intro": "Виберіть сумісний хмарний сервіс для цієї перевірки.",
2121
"cloud_learning_source_smartess": "SmartESS API",
2222
"cloud_learning_source_dessmonitor": "DESSMonitor API",
23+
"cloud_learning_source_smartclient": "SmartClient / ShineMonitor",
2324
"cloud_learning_source_valuecloud": "ValueCloud API",
2425
"cloud_learning_metadata_done": "Аналіз пристрою лише для читання завершено.",
2526
"local_register_observation_available": "За бажанням можна близько {minutes} хвилин збирати локальні показання у фоні. Це лише дані для перевірки: сутності та елементи керування не додаються.",
@@ -388,13 +389,13 @@
388389
"support_workflow_pending_step_2": "Надішліть ZIP-файл розробнику.",
389390
"support_workflow_pending_step_3": "Переходьте до локальних чернеток лише після того, як розробник перегляне зібрані дані.",
390391
"support_workflow_pending_advanced_hint": "Розглядайте локальні чернетки тут як інструмент другого етапу. Спочатку підтвердьте з'єднання або надішліть докази підтримки.",
391-
"support_workflow_smartess_pending_level_label": "Є ознаки SmartESS",
392-
"support_workflow_smartess_pending_summary": "Колектор повернув метадані SmartESS, але локальний драйвер інвертора поки не зіставлено.",
393-
"support_workflow_smartess_pending_next_action": "Створіть архів підтримки та надішліть ZIP-файл розробнику. Підтримка в застосунку SmartESS усе ще можлива через окреме хмарне зіставлення пристрою, навіть якщо локальний ідентифікатор загальний, порожній або неповний.",
392+
"support_workflow_smartess_pending_level_label": "Є дані протоколу колектора",
393+
"support_workflow_smartess_pending_summary": "Колектор повернув дані протоколу, але локальний драйвер інвертора поки не визначено.",
394+
"support_workflow_smartess_pending_next_action": "Створіть архів підтримки та надішліть ZIP-файл розробнику. Застосунок виробника може визначати цей інвертор через хмару, навіть якщо локальні дані неповні.",
394395
"support_workflow_smartess_pending_step_1": "Створіть архів підтримки.",
395396
"support_workflow_smartess_pending_step_2": "Надішліть ZIP-файл розробнику.",
396-
"support_workflow_smartess_pending_step_3": "Сприймайте локальний ідентифікатор SmartESS лише як дані колектора, доки не буде підтверджено вбудоване зіставлення.",
397-
"support_workflow_smartess_pending_advanced_hint": "Не сприймайте загальний або порожній локальний ідентифікатор SmartESS як доказ того, що пристрій не підтримується застосунком SmartESS.",
397+
"support_workflow_smartess_pending_step_3": "Дані протоколу колектора ще не підтверджують підтримку інвертора.",
398+
"support_workflow_smartess_pending_advanced_hint": "Ці дані не визначають хмарний застосунок або локальну карту регістрів. Для збору хмарних даних обирайте застосунок, яким ви користуєтеся.",
398399
"support_workflow_family_fallback_level_label": "Неперевірений read-only пристрій сімейства SMG",
399400
"support_workflow_family_fallback_summary": "Для цього інвертора використовується загальний fallback-профіль сімейства SMG. Вбудовані записи навмисно вимкнені, доки точну модель не буде підтверджено.",
400401
"support_workflow_family_fallback_next_action": "Створіть архів підтримки та надішліть ZIP-файл розробнику. Це допоможе підтвердити точну модель сімейства SMG і вивести пристрій із read-only fallback-режиму.",

‎custom_components/eybond_local/flows/options/shadow_run.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ async def _async_continue_after_learning_source(self, coordinator) -> ConfigFlow
276276

277277
def _control_discovery_source_option_label(self, source_id: str) -> str:
278278
labels = {
279+
"smartclient": self._tr(
280+
"common.dynamic.cloud_learning_source_smartclient",
281+
"SmartClient / ShineMonitor",
282+
),
279283
"smartess": self._tr(
280284
"common.dynamic.cloud_learning_source_smartess",
281285
"SmartESS API",
@@ -377,7 +381,8 @@ async def async_step_shadow_learning_credentials(
377381
errors: dict[str, str] = {}
378382
defaults = dict(user_input or {})
379383
username = str(defaults.get("username") or "").strip()
380-
password = str(defaults.get("password") or "").strip()
384+
# Whitespace can be part of the cloud password and its signed hash.
385+
password = str(defaults.get("password") or "")
381386
if user_input is not None:
382387
if not username:
383388
errors["username"] = "required"

0 commit comments

Comments
 (0)