Code quality audit: THZ integration (iterative assessment)#96
Code quality audit: THZ integration (iterative assessment)#96Copilot wants to merge 1 commit intodevelopmentfrom
Conversation
|
analyze again with the current state of development branch |
Here is the updated assessment against the current tip of Code Quality Assessment —
|
| Commit | Change | Quality impact |
|---|---|---|
1d8340f |
Remove Monthly + Yearly COP sensors | ✅ cop_sensor.py shrunk from 886 → 577 lines, 5 classes → 3 |
992b25b |
Fix area schema to use vol.In |
✅ Minor correctness fix |
710f2bb |
Add BLOCK_LABELS dict to const.py + translations |
➡ Neutral — adds 61-line dict to const.py, improves UI |
0f4d664 |
Fix ventilator speed unit/device_class | ✅ Correctness fix in sensor_meta.py |
Metrics at a Glance (dev branch vs previous)
| Metric | Previous | Now | Δ |
|---|---|---|---|
| Production lines | 11,704 | 11,486 | −218 ✅ |
| Test functions | 386 | 393 | +7 ✅ |
| Tests passing | 371 | 378 | +7 ✅ |
| Tests failing | 15 | 15 | 0 (same failures) |
| Lint errors (ruff) | 86 | 80 | −6 ✅ |
Broad except Exception |
13 | 13 | 0 |
| Functions > 50 lines | 20 | 20 | 0 |
| High-CC functions (≥10) | 10 | 14 | +4 |
| Type hint coverage | 80% | 81% | +1% ✅ |
| Test/code ratio | 0.37x | 0.38x | ≈same |
Findings — Status by Category
✅ Resolved / Improved
cop_sensor.pyreduced from 886 → 577 lines by removing unused Monthly/Yearly COP classes. Thenative_valueCC≈11 offender from the previous report (the monthly/yearly classes) is now gone.- Ventilator speed sensor now has correct
device_classand unit. - Area schema fixed to use
vol.In(stricter, correct). - 7 new tests added, all passing.
⚠️ Still Open (unchanged from previous report)
-
decode_valueduplicated — still copy-pasted insensor.pyandcop_sensor.py.value_codec.pyexists for this. Thecop_sensor.pycopy still has the comment "duplicated to avoid circular imports" even though importing fromvalue_codechas no circularity at all. -
hass.datamulti-instance safety —device,device_id,write_manager,register_managerare still stored athass.data[DOMAIN](global, not perentry_id). A second config entry would overwrite the first. The duplicatesetdefaultis now actually three calls (lines 44, 86, 156) instead of two. -
sensor_meta.py— still 971 lines / 97 entries. The file's module docstring still claims to be the metadata store, while register map tuples also carry metadata via a 6th element. No single source of truth. -
13 bare
except Exception— identical count and locations as before. No narrowing of exception types. -
20 functions > 50 lines — same list.
send_request()(136 lines, CC≈26) and_list_serial_ports()(96 lines, CC≈26) remain the worst offenders. -
High-CC function count increased from 10 → 14 —
_async_enable_integration_disabled_entities()(CC≈14) andshould_hide_entity_by_default()(CC≈11, grown due to newBLOCK_LABELScontent inconst.py) are newly flagged. The functionshould_hide_entity_by_defaultat CC≈11 / 58 lines is itself a smell — hiding logic should be data-driven (a set of keywords), not a chain ofifbranches. -
asyncio.sleep(0.01)insideasync with device.lock— still present in 9 places acrossswitch.py,select.py,number.py,time.py,calendar.py,schedule.py. -
15 failing tests — identical set. 13 fail due to
pytest-asyncionot being inrequirements_test.txt. 2 fail due to incomplete mock intest_thz_device_extended.py. These are non-functional tests that appear to be written but never run. -
has_entity_nameonly conditionally set (when atranslation_keyexists) — entity naming is inconsistent between translated and untranslated entities. -
1 live f-string in a
_LOGGERcall —thz_device.py:630uses_LOGGER.error(f"...")instead of_LOGGER.error("...", e). 6 commented-out_LOGGERdebug lines also remain inthz_device.py. -
4 internal markdown files in repo root —
BUGFIX_CONNECTION_TIMEOUT.md,CODE_REVIEW_SUMMARY.md,IMPLEMENTATION_SUMMARY.md,REFACTORING_SUMMARY.mdare developer artefacts, not user docs. Should move todocs/or be removed.
Priority Order for Next Steps
| Priority | Item |
|---|---|
| High | Fix hass.data global-vs-per-entry (multi-instance safety) |
| High | Add pytest-asyncio to requirements_test.txt to un-fail 13 tests |
| Medium | Consolidate decode_value into value_codec.py |
| Medium | Narrow the 13 except Exception to specific types |
| Medium | Make should_hide_entity_by_default data-driven (set of keywords/prefixes) to reduce CC |
| Low | Move the 4 internal .md files out of repo root |
| Low | Fix the lone f-string logger call + remove 6 commented-out debug lines in thz_device.py |
Overall grade: C+ → B− (slight improvement due to COP cleanup and test additions, but the structural issues remain untouched).
|
@copilot assess again. |
Here is the updated assessment against the current tip of Code Quality Assessment —
|
| Area | Change | Impact |
|---|---|---|
sensor_meta.py |
Deleted | ✅ Single source of metadata truth (register map tuples) |
schedule.py |
Deleted (dead code) | ✅ Removes ~250 lines of never-loaded code |
calendar.py |
Deleted (dead code) | ✅ Removes ~425 lines of never-loaded code |
decode_value |
Consolidated — cop_sensor.py now imports from value_codec.py; test_helpers.py is now a 1-line re-export |
✅ Duplication fully resolved |
base_entity.py |
_attr_should_poll=False with async_track_time_interval |
✅ Correct HA polling pattern |
hass.data.setdefault |
Reduced from 3 → 2 calls | ✅ One duplicate removed |
cop_sensor.py |
Added THZBaseCOPSensor base class; Monthly/Yearly removed |
✅ Better structure, 309 fewer lines |
value_codec.py |
Added somwinmode, weekday, hexdate, clockdate decoders |
➡ Growth adds CC complexity |
register_map_all.py |
Massively expanded with inline metadata tuples | ➡ Generates 183 new E501 errors |
thz_device_extended tests |
Fixed — all 27 now pass | ✅ |
| Dead-code helpers removed | get_device_from_hass, read_block_cached deleted |
✅ |
Metrics at a Glance
| Metric | Previous (0f4d664) | Now (1014460) | Δ |
|---|---|---|---|
| Production Python files | 37 | 34 | −3 ✅ |
| Production lines | 11,486 | 10,518 | −968 ✅ |
| Test functions | 393 | 375 | −18 (removed tests for deleted modules) |
| Tests passing | 378 | 362 | −16 (same ratio — deleted module tests removed) |
| Tests failing | 15 | 13 | −2 ✅ |
| Lint errors (ruff) | 80 | 319 | +239 |
Broad except Exception |
13 | 9 | −4 ✅ |
| Functions > 50 lines | 20 | 19 | −1 ✅ |
| High-CC functions (≥10) | 14 | 12 | −2 ✅ |
| Type hint coverage | 81% | 79% | −2% |
| Test/code ratio | 0.38x | 0.41x | +0.03x ✅ |
Findings — Status by Category
✅ Resolved Since Last Assessment
decode_valueduplication — fully resolved.value_codec.pyownsdecode_raw_value().sensor.pyexports it as a compat wrapper.cop_sensor.pyimports directly.test_helpers.pyis now a clean 1-line re-export.sensor_meta.py— deleted. Register map tuple 6th element is now the sole metadata source.schedule.py/calendar.py— dead code removed (~675 lines).thz_device_extendedmock failures — fixed; all 27 tests pass._attr_should_poll— correctlyFalse; write entities useasync_track_time_interval.- Duplicate
setdefault— down to 2 calls (still one redundant on line 43 and 157, but much better). except Exceptioncount reduced from 13 → 9 (removed with calendar/schedule).
⚠️ Newly Introduced Issues
Lint errors spike: 80 → 319 — almost entirely E501 (line too long). This comes from register_map_206.py (146 violations) and register_map_all.py (34 violations) where inline metadata tuples produce very long lines like:
("mixerOpen:", 45, 1, "bit0", 1, {"icon": "mdi:gate-open", "translation_key": "mixer_open"}),The pyproject.toml excludes docstring rules for register_maps/ but not E501. Consider either raising the line limit for register map files or wrapping the metadata dict onto its own line. This is the single biggest quality regression.
decode_raw_value() (value_codec.py) CC≈14, 63 lines — the function gained new decode branches (somwinmode, weekday, hexdate, clockdate, opmodehc, 8party). With 14+ branches it is hard to read and test each path independently. A dispatch dict {decode_type: handler} would cut this to CC≈1.
⚠️ Still Open (unchanged)
hass.datamulti-instance safety —device,device_id,write_manager,register_managerstill stored athass.data[DOMAIN](global, not perentry_id). Two config entries would still collide.- 9 bare
except Exception— still inthz_device.py(6×),__init__.py(2×),config_flow.py(1×). send_request()CC≈26, 136 lines and_list_serial_ports()CC≈26, 96 lines — unchanged.asyncio.sleep(0.01)insideasync with device.lock— still in 7 places (switch, select, number, time ×4).- 13 tests still failing (all 13 due to missing
pytest-asyncioinrequirements_test.txt). - 1 live f-string logger call —
thz_device.py:603still uses_LOGGER.error(f"..."). - 6 commented-out
_LOGGERlines inthz_device.py— still present. - 4 internal
.mdfiles in repo root —BUGFIX_CONNECTION_TIMEOUT.md,CODE_REVIEW_SUMMARY.md,IMPLEMENTATION_SUMMARY.md,REFACTORING_SUMMARY.md. ACHANGELOG.mdwas added (appropriate), but the others remain clutter.
Priority Order
| Priority | Item |
|---|---|
| High | Fix E501 spike in register maps (319 → ~0 lint errors) |
| High | Add pytest-asyncio to requirements_test.txt (un-fail 13 tests) |
| Medium | Fix hass.data multi-instance safety (per-entry_id storage) |
| Medium | Refactor decode_raw_value() as a dispatch dict to reduce CC |
| Medium | Narrow remaining 9 except Exception catches |
| Low | Remove 4 internal .md files from repo root |
| Low | Fix lone f-string logger + 6 commented-out debug lines in thz_device.py |
Overall grade: C+ → B− (significant structural improvements, but the lint regression from the register map expansion needs attention).
Three successive assessments of the THZ integration codebase, tracking quality metrics and providing a prioritised backlog of issues. No code was changed in this PR — findings fed directly into separate fix PRs merged to
development.Assessment Snapshots
except Exception(bare)Issues Identified and Since Fixed
decode_valuetriplication —sensor.py,cop_sensor.py,test_helpers.pyall had independent copies; consolidated intovalue_codec.decode_raw_valuewith a compat wrapperhass.datamulti-instance unsafe — globalhass.data[DOMAIN]["device"]storage would corrupt a second config entry; moved tohass.data[DOMAIN][entry_id]calendar.py,schedule.py,sensor_meta.pywere never loaded by the integration (not inasync_forward_entry_setups); deletedasyncio.sleep(0.01)insideasync with device.lock— held the lock for an extra 10ms on every write; removed all 7 occurrencessend_request()/_list_serial_ports()CC≈26 — decomposed into focused helper methods (_do_handshake_1/2,_exchange_once,_build_by_id_map, etc.)decode_raw_value()CC≈14 — replaced 14-branch if/elif chain with a_DECODE_DISPATCHdict; CC now ≈3requirements_test.txt— 13 async tests silently never ranper-file-ignoresforregister_maps/*.pyStill Open (low priority)
_async_handle_read_raw_register(184 lines) is a nested closure inside_async_setup_services— untestable in isolation; candidate for extraction to module levelhas_entity_name=Trueconditionally gated ontranslation_keypresence — inconsistent with HA naming conventions for legacy entitiesnumber,switch,select,time)💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.