python-bsblan 6.1.8__tar.gz → 6.1.9__tar.gz
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.
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/renovate.json +8 -5
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/codeql.yaml +2 -2
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/docs.yaml +1 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/release-drafter.yaml +1 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/release.yaml +3 -3
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/scorecard.yml +1 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/stale.yaml +1 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/tests.yaml +4 -4
- python_bsblan-6.1.9/.nvmrc +1 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/PKG-INFO +2 -2
- python_bsblan-6.1.9/examples/fake_bsblan_server.py +522 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/profile_init.py +2 -2
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/ruff.toml +4 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/package-lock.json +4 -4
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/package.json +1 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/pyproject.toml +11 -10
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/bsblan.py +11 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_pps.py +34 -1
- python_bsblan-6.1.8/.nvmrc +0 -1
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.editorconfig +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.gitattributes +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/CODE_OF_CONDUCT.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/CONTRIBUTING.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/SECURITY.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/agents/pr-review.agent.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/agents/security-reviewer.agent.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/agents/tdd-refactor.agent.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/copilot-instructions.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/labels.yml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/add-parameter.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/code-review.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/opsx-apply.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/opsx-archive.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/opsx-explore.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/opsx-propose.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/prompts/opsx-sync.prompt.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/release-drafter.yml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/bsblan-parameters/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/bsblan-testing/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/feature-doc-updates/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/openspec-apply-change/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/openspec-archive-change/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/openspec-explore/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/openspec-propose/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/skills/openspec-sync-specs/SKILL.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/auto-approve-renovate.yml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/dependency-review.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/labels.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/linting.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/lock.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/pr-labels.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/workflows/typing.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.github/zizmor.yml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.gitignore +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.pre-commit-config.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.prettierignore +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/.yamllint +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/AGENTS.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/CLAUDE.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/LICENSE.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/Makefile +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/README.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/api/client.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/api/constants.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/api/exceptions.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/api/models.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/getting-started.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/docs/index.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/control.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/discovery.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/fetch_param.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/examples/speed_test.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/mkdocs.yml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/.openspec.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/design.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/proposal.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/specs/cooling-operating-mode/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/tasks.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/.openspec.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/design.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/proposal.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/specs/circuit-discovery/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/tasks.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/.openspec.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/design.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/proposal.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/specs/client-retry-policy/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/tasks.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/.openspec.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/design.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/proposal.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/specs/error-classification/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/tasks.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/.openspec.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/design.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/proposal.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/specs/lazy-validation-completeness/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/tasks.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/config.yaml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/specs/circuit-discovery/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/specs/client-retry-policy/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/specs/cooling-operating-mode/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/specs/error-classification/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/openspec/specs/lazy-validation-completeness/spec.md +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/sonar-project.properties +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/__init__.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_hot_water.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_parameters.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_schedules.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_temperature.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_thermostat.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_transport.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_validation.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/_version.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/constants.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/exceptions.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/models.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/py.typed +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/src/bsblan/utility.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/__init__.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/conftest.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/device.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/dict_version.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/hot_water_state.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/info.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/password.txt +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/pps_device.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/pps_state.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/pps_static_values.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/sensor.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/state.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/state_circuit2.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/static_state.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/static_state_circuit2.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/thermostat_hvac.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/thermostat_temp.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/fixtures/time.json +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/ruff.toml +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_api_initialization.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_api_validation.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_auth.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_backoff_retry.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_bsblan.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_bsblan_edge_cases.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_circuit.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_configuration.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_constants.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_context_manager.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_device.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_dhw_time_switch.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_entity_info.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_entity_info_ha.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_heating_schedule.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_hot_water_additional.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_hotwater_state.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_include_parameter.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_info.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_initialization.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_read_parameters.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_reset_validation.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_response_encoding.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_schedule_models.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_sensor.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_set_heating_schedule.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_set_hot_water_schedule.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_set_hotwater.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_state.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_static_state.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_temperature_unit.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_temperature_validation.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_thermostat.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_time.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_utility.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_utility_additional.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_utility_edge_cases.py +0 -0
- {python_bsblan-6.1.8 → python_bsblan-6.1.9}/tests/test_version_errors.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
3
|
"schedule": ["before 2am"],
|
|
4
|
+
"minimumReleaseAge": "3 days",
|
|
4
5
|
"rebaseWhen": "behind-base-branch",
|
|
5
6
|
"dependencyDashboard": true,
|
|
6
7
|
"labels": ["dependencies", "no-stale"],
|
|
@@ -11,17 +12,19 @@
|
|
|
11
12
|
"commitMessagePrefix": "⬆️",
|
|
12
13
|
"packageRules": [
|
|
13
14
|
{
|
|
14
|
-
"
|
|
15
|
+
"description": "UV project dependencies",
|
|
16
|
+
"matchFileNames": ["pyproject.toml", "uv.lock"],
|
|
15
17
|
"addLabels": ["python"]
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
+
"description": "Pin UV development dependencies",
|
|
21
|
+
"matchFileNames": ["pyproject.toml", "uv.lock"],
|
|
22
|
+
"matchDepTypes": ["dependency-groups"],
|
|
20
23
|
"rangeStrategy": "pin"
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
|
-
"description": "Automerge minor/patch
|
|
24
|
-
"
|
|
26
|
+
"description": "Automerge minor/patch UV dependency updates (after approval and CI)",
|
|
27
|
+
"matchFileNames": ["pyproject.toml", "uv.lock"],
|
|
25
28
|
"matchUpdateTypes": ["minor", "patch"],
|
|
26
29
|
"automerge": true
|
|
27
30
|
},
|
|
@@ -27,6 +27,6 @@ jobs:
|
|
|
27
27
|
with:
|
|
28
28
|
persist-credentials: false
|
|
29
29
|
- name: 🏗 Initialize CodeQL
|
|
30
|
-
uses: github/codeql-action/init@
|
|
30
|
+
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
31
31
|
- name: 🚀 Perform CodeQL Analysis
|
|
32
|
-
uses: github/codeql-action/analyze@
|
|
32
|
+
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
steps:
|
|
37
37
|
- name: 🚀 Run Release Drafter
|
|
38
38
|
# yamllint disable-line rule:line-length
|
|
39
|
-
uses: release-drafter/release-drafter@
|
|
39
|
+
uses: release-drafter/release-drafter@72967cdc98ddd3160f1fc3dff619de365e137dd0 # v7.9.0
|
|
40
40
|
with:
|
|
41
41
|
prerelease: ${{ github.event.inputs.prerelease == 'true' }}
|
|
42
42
|
prerelease-identifier: ${{ github.event.inputs.prerelease_identifier }}
|
|
@@ -50,16 +50,16 @@ jobs:
|
|
|
50
50
|
- name: 🏗 Build package
|
|
51
51
|
run: uv build
|
|
52
52
|
- name: 🔏 Attest build provenance
|
|
53
|
-
uses: actions/attest-build-provenance@
|
|
53
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
54
54
|
with:
|
|
55
55
|
subject-path: ./dist/*
|
|
56
56
|
- name: 🚀 Publish to PyPi
|
|
57
|
-
uses: pypa/gh-action-pypi-publish@
|
|
57
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
58
58
|
with:
|
|
59
59
|
verbose: true
|
|
60
60
|
print-hash: true
|
|
61
61
|
- name: ✍️ Sign published artifacts
|
|
62
|
-
uses: sigstore/gh-action-sigstore-python@
|
|
62
|
+
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
|
|
63
63
|
with:
|
|
64
64
|
inputs: ./dist/*.tar.gz ./dist/*.whl
|
|
65
65
|
release-signing-artifacts: false
|
|
@@ -66,6 +66,6 @@ jobs:
|
|
|
66
66
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
|
67
67
|
- name: "Upload to code-scanning"
|
|
68
68
|
# yamllint disable-line rule:line-length
|
|
69
|
-
uses: github/codeql-action/upload-sarif@
|
|
69
|
+
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
70
70
|
with:
|
|
71
71
|
sarif_file: results.sarif
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
pull-requests: write
|
|
19
19
|
steps:
|
|
20
20
|
- name: 🚀 Run stale
|
|
21
|
-
uses: actions/stale@
|
|
21
|
+
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
|
22
22
|
with:
|
|
23
23
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
24
24
|
days-before-stale: 30
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
fetch-depth: 0
|
|
67
67
|
persist-credentials: false
|
|
68
68
|
- name: ⬇️ Download coverage data
|
|
69
|
-
uses: actions/download-artifact@
|
|
69
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
70
70
|
- name: 🏗 Set up uv
|
|
71
71
|
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
72
72
|
with:
|
|
@@ -84,14 +84,14 @@ jobs:
|
|
|
84
84
|
uv run python -m coverage xml -i
|
|
85
85
|
- name: 🚀 Upload coverage report
|
|
86
86
|
if: env.HAS_CODECOV_TOKEN == 'true'
|
|
87
|
-
uses: codecov/codecov-action@
|
|
87
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
88
88
|
with:
|
|
89
89
|
fail_ci_if_error: true
|
|
90
90
|
files: coverage.xml
|
|
91
91
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
92
92
|
- name: 🚀 Upload coverage report (tokenless)
|
|
93
93
|
if: env.HAS_CODECOV_TOKEN != 'true' && github.event_name == 'pull_request'
|
|
94
|
-
uses: codecov/codecov-action@
|
|
94
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
95
95
|
with:
|
|
96
96
|
fail_ci_if_error: false
|
|
97
97
|
files: coverage.xml
|
|
@@ -101,7 +101,7 @@ jobs:
|
|
|
101
101
|
- name: SonarQube Cloud Scan
|
|
102
102
|
if: env.HAS_SONAR_TOKEN == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork)
|
|
103
103
|
# yamllint disable-line rule:line-length
|
|
104
|
-
uses: SonarSource/sonarqube-scan-action@
|
|
104
|
+
uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3
|
|
105
105
|
env:
|
|
106
106
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
107
107
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.21.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-bsblan
|
|
3
|
-
Version: 6.1.
|
|
3
|
+
Version: 6.1.9
|
|
4
4
|
Summary: Asynchronous Python client for BSBLAN API
|
|
5
5
|
Project-URL: Homepage, https://github.com/liudger/python-bsblan
|
|
6
6
|
Project-URL: Repository, https://github.com/liudger/python-bsblan
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Fake BSB-LAN server for testing multi-circuit config flows.
|
|
2
|
+
|
|
3
|
+
Simulates a BSB-LAN device with 2 (or 3) heating circuits.
|
|
4
|
+
All three endpoints (/JQ, /JI, /JS) are implemented.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python script/fake_bsblan_server.py [--port 8080] [--circuits 2] [--passkey KEY]
|
|
8
|
+
[--omit-static-values] [--omit-dhw]
|
|
9
|
+
[--json-api-version 2.0 | --json-api-version 1.0]
|
|
10
|
+
[--json-api-unavailable]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--omit-static-values Omit heating circuit min/max temperature parameters
|
|
14
|
+
from responses, simulating devices that don't support
|
|
15
|
+
static values.
|
|
16
|
+
--omit-dhw Omit all DHW (Domestic Hot Water) parameters from
|
|
17
|
+
responses, simulating devices without hot water support.
|
|
18
|
+
--firmware-version Override the reported firmware version. Use a v1
|
|
19
|
+
version (e.g. 1.0.0) to trigger the outdated firmware
|
|
20
|
+
repair issue.
|
|
21
|
+
--json-api-version Value returned by /JV as the JSON-API version
|
|
22
|
+
(default: 2.0).
|
|
23
|
+
--json-api-unavailable Simulate a device where /JV cannot be retrieved.
|
|
24
|
+
The endpoint will return HTTP 404.
|
|
25
|
+
|
|
26
|
+
Then add the integration in Home Assistant UI with:
|
|
27
|
+
Host: 127.0.0.1 (or localhost)
|
|
28
|
+
Port: 8080 (or whatever you set)
|
|
29
|
+
Passkey: (leave blank unless --passkey was set)
|
|
30
|
+
|
|
31
|
+
The config flow should detect multiple circuits and show the selection step.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from aiohttp import web
|
|
42
|
+
|
|
43
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
44
|
+
_LOGGER = logging.getLogger("fake_bsblan")
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Device identity
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
DEVICE_INFO: dict[str, Any] = {
|
|
50
|
+
"name": "BSB-LAN",
|
|
51
|
+
"version": "3.3.2-20250101120000",
|
|
52
|
+
"freeram": 42000,
|
|
53
|
+
"uptime": 123456789,
|
|
54
|
+
"MAC": "00:80:41:DE:AD:01",
|
|
55
|
+
"freespace": 0,
|
|
56
|
+
"bus": "BSB",
|
|
57
|
+
"buswritable": 1,
|
|
58
|
+
"busaddr": 66,
|
|
59
|
+
"busdest": 0,
|
|
60
|
+
"monitor": 0,
|
|
61
|
+
"verbose": 1,
|
|
62
|
+
"protectedGPIO": [{"pin": 0}],
|
|
63
|
+
"averages": [],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Parameter database — keyed by parameter ID string
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _param(
|
|
72
|
+
name: str,
|
|
73
|
+
value: str,
|
|
74
|
+
*,
|
|
75
|
+
desc: str = "",
|
|
76
|
+
data_type: int = 0,
|
|
77
|
+
readonly: int = 0,
|
|
78
|
+
unit: str = "",
|
|
79
|
+
precision: float | None = None,
|
|
80
|
+
readwrite: int = 0,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Build a single parameter response entry."""
|
|
83
|
+
entry: dict[str, Any] = {
|
|
84
|
+
"name": name,
|
|
85
|
+
"error": 0,
|
|
86
|
+
"value": value,
|
|
87
|
+
"desc": desc,
|
|
88
|
+
"dataType": data_type,
|
|
89
|
+
"readonly": readonly,
|
|
90
|
+
"unit": unit,
|
|
91
|
+
}
|
|
92
|
+
if precision is not None:
|
|
93
|
+
entry["precision"] = precision
|
|
94
|
+
entry["readwrite"] = readwrite
|
|
95
|
+
return entry
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _inactive_param(name: str) -> dict[str, Any]:
|
|
99
|
+
"""Build an inactive/unavailable parameter entry."""
|
|
100
|
+
return {
|
|
101
|
+
"name": name,
|
|
102
|
+
"error": 0,
|
|
103
|
+
"value": "0",
|
|
104
|
+
"desc": "---",
|
|
105
|
+
"dataType": 1,
|
|
106
|
+
"readonly": 1,
|
|
107
|
+
"unit": "",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_parameter_db(
|
|
112
|
+
num_circuits: int, *, omit_dhw: bool = False
|
|
113
|
+
) -> dict[str, dict[str, Any]]:
|
|
114
|
+
"""Build the full parameter database for the simulated device.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
num_circuits: Number of active heating circuits (1, 2, or 3).
|
|
118
|
+
omit_dhw: If True, omit all DHW parameters (no hot water support).
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
db: dict[str, dict[str, Any]] = {}
|
|
122
|
+
|
|
123
|
+
# ---- Device info params (6224-6226) ----
|
|
124
|
+
db["6224"] = _param("Device identification", "RVS21.831F/127", data_type=7)
|
|
125
|
+
db["6225"] = _param("Device family", "211")
|
|
126
|
+
db["6226"] = _param("Device variant", "127")
|
|
127
|
+
|
|
128
|
+
# ---- Sensor params ----
|
|
129
|
+
db["8700"] = _param("Outside temp sensor local", "6.1", unit="°C", readonly=1)
|
|
130
|
+
db["8740"] = _param("Room temp 1 actual value", "21.3", unit="°C", readonly=1)
|
|
131
|
+
db["3113"] = _param("Total energy", "7968", unit="kWh", readonly=1)
|
|
132
|
+
|
|
133
|
+
# ---- Time param (0) ----
|
|
134
|
+
db["0"] = _param("Date/time", "02.03.2026 14:00:00", data_type=5)
|
|
135
|
+
|
|
136
|
+
# ---- Hot water params ----
|
|
137
|
+
if not omit_dhw:
|
|
138
|
+
db["1600"] = _param("DHW operating mode", "1", desc="On", data_type=1)
|
|
139
|
+
db["1601"] = _param("DHW eco mode selection", "0", desc="Off", data_type=1)
|
|
140
|
+
db["1610"] = _param("DHW nominal setpoint", "50.0", unit="°C")
|
|
141
|
+
db["1612"] = _param("DHW reduced setpoint", "40.0", unit="°C")
|
|
142
|
+
db["1614"] = _param("DHW nominal setpoint max", "65.0", unit="°C")
|
|
143
|
+
db["1620"] = _param("DHW release", "1", desc="Released", data_type=1)
|
|
144
|
+
db["1630"] = _param(
|
|
145
|
+
"DHW charging priority", "1", desc="DHW priority", data_type=1
|
|
146
|
+
)
|
|
147
|
+
db["1640"] = _param("Legionella function", "0", desc="Off", data_type=1)
|
|
148
|
+
db["1641"] = _param("Legionella periodicity", "4", desc="4 weeks", data_type=1)
|
|
149
|
+
db["1642"] = _param("Legionella day", "6", desc="Saturday", data_type=3)
|
|
150
|
+
db["1644"] = _param("Legionella time", "03:00", data_type=4)
|
|
151
|
+
db["1645"] = _param("Legionella setpoint", "70.0", unit="°C")
|
|
152
|
+
db["1646"] = _param("Legionella dwelling time", "5", unit="min")
|
|
153
|
+
db["1647"] = _param("Legionella circulation pump", "1", desc="On", data_type=1)
|
|
154
|
+
db["1648"] = _param("Legionella circulation temp diff", "0", unit="K")
|
|
155
|
+
db["1660"] = _param(
|
|
156
|
+
"DHW circulation pump release", "1", desc="24H/Day", data_type=1
|
|
157
|
+
)
|
|
158
|
+
db["1661"] = _param(
|
|
159
|
+
"DHW circulation pump cycling", "1", desc="1x 3min/h", data_type=1
|
|
160
|
+
)
|
|
161
|
+
db["1663"] = _param("DHW circulation setpoint", "40.0", unit="°C")
|
|
162
|
+
db["1680"] = _param("DHW changeover", "0", desc="---", data_type=1)
|
|
163
|
+
db["8830"] = _param("DHW actual top temp", "48.2", unit="°C", readonly=1)
|
|
164
|
+
db["8820"] = _param(
|
|
165
|
+
"DHW pump state", "255", desc="Off", data_type=1, readonly=1
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# DHW schedule params
|
|
169
|
+
for pid, day in [
|
|
170
|
+
("561", "Monday"),
|
|
171
|
+
("562", "Tuesday"),
|
|
172
|
+
("563", "Wednesday"),
|
|
173
|
+
("564", "Thursday"),
|
|
174
|
+
("565", "Friday"),
|
|
175
|
+
("566", "Saturday"),
|
|
176
|
+
("567", "Sunday"),
|
|
177
|
+
]:
|
|
178
|
+
db[pid] = _param(
|
|
179
|
+
f"DHW time program {day}",
|
|
180
|
+
"06:00-22:00 ##:##-##:## ##:##-##:##",
|
|
181
|
+
data_type=9,
|
|
182
|
+
)
|
|
183
|
+
db["576"] = _param(
|
|
184
|
+
"DHW time program standard values", "0", desc="No", data_type=1
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ---- Heating Circuit 1 (700-series) — always active ----
|
|
188
|
+
db["700"] = _param("HC1 Operating mode", "3", desc="Comfort", data_type=1)
|
|
189
|
+
db["710"] = _param("HC1 Room temp comfort setpoint", "21.0", unit="°C")
|
|
190
|
+
db["714"] = _param("HC1 Room temp frost protection", "8.0", unit="°C")
|
|
191
|
+
db["716"] = _param("HC1 Max temp", "26.0", unit="°C")
|
|
192
|
+
db["730"] = _param("HC1 Summer/winter changeover", "20.0", unit="°C")
|
|
193
|
+
db["770"] = _param("HC1 Room1 temp setpoint boost", "0.0", unit="°C")
|
|
194
|
+
db["900"] = _param("HC1 Changeover mode", "0", desc="---", data_type=1)
|
|
195
|
+
db["8000"] = _param(
|
|
196
|
+
"Status heating circuit 1",
|
|
197
|
+
"122",
|
|
198
|
+
desc="Room temp limiting",
|
|
199
|
+
data_type=1,
|
|
200
|
+
readonly=1,
|
|
201
|
+
)
|
|
202
|
+
db["8749"] = _param(
|
|
203
|
+
"HC1 Room1 thermostat mode", "0", desc="No demand", data_type=1, readonly=1
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ---- Heating Circuit 2 (1000-series) ----
|
|
207
|
+
if num_circuits >= 2:
|
|
208
|
+
db["1000"] = _param("HC2 Operating mode", "1", desc="Auto", data_type=1)
|
|
209
|
+
db["1010"] = _param("HC2 Room temp comfort setpoint", "19.0", unit="°C")
|
|
210
|
+
db["1014"] = _param("HC2 Room temp frost protection", "8.0", unit="°C")
|
|
211
|
+
db["1016"] = _param("HC2 Max temp", "24.0", unit="°C")
|
|
212
|
+
db["1030"] = _param("HC2 Summer/winter changeover", "20.0", unit="°C")
|
|
213
|
+
db["1070"] = _param("HC2 Room1 temp setpoint boost", "0.0", unit="°C")
|
|
214
|
+
db["1200"] = _param("HC2 Changeover mode", "0", desc="---", data_type=1)
|
|
215
|
+
db["8001"] = _param(
|
|
216
|
+
"Status heating circuit 2",
|
|
217
|
+
"100",
|
|
218
|
+
desc="Heating",
|
|
219
|
+
data_type=1,
|
|
220
|
+
readonly=1,
|
|
221
|
+
)
|
|
222
|
+
db["8741"] = _param(
|
|
223
|
+
"Room temp 2 actual value", "19.8", unit="°C", readonly=1
|
|
224
|
+
)
|
|
225
|
+
db["8750"] = _param(
|
|
226
|
+
"HC2 Room1 thermostat mode",
|
|
227
|
+
"1",
|
|
228
|
+
desc="Comfort",
|
|
229
|
+
data_type=1,
|
|
230
|
+
readonly=1,
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
# Inactive HC2
|
|
234
|
+
db["1000"] = _inactive_param("HC2 Operating mode")
|
|
235
|
+
db["8001"] = _inactive_param("Status heating circuit 2")
|
|
236
|
+
|
|
237
|
+
# ---- Heating Circuit 3 (1300-series) ----
|
|
238
|
+
if num_circuits >= 3:
|
|
239
|
+
db["1300"] = _param("HC3 Operating mode", "1", desc="Auto", data_type=1)
|
|
240
|
+
db["1310"] = _param("HC3 Room temp comfort setpoint", "18.0", unit="°C")
|
|
241
|
+
db["1314"] = _param("HC3 Room temp frost protection", "8.0", unit="°C")
|
|
242
|
+
db["1316"] = _param("HC3 Max temp", "22.0", unit="°C")
|
|
243
|
+
db["1330"] = _param("HC3 Summer/winter changeover", "20.0", unit="°C")
|
|
244
|
+
db["1370"] = _param("HC3 Room1 temp setpoint boost", "0.0", unit="°C")
|
|
245
|
+
db["1500"] = _param("HC3 Changeover mode", "0", desc="---", data_type=1)
|
|
246
|
+
db["8002"] = _param(
|
|
247
|
+
"Status heating circuit 3",
|
|
248
|
+
"100",
|
|
249
|
+
desc="Heating",
|
|
250
|
+
data_type=1,
|
|
251
|
+
readonly=1,
|
|
252
|
+
)
|
|
253
|
+
db["8742"] = _param(
|
|
254
|
+
"Room temp 3 actual value", "17.5", unit="°C", readonly=1
|
|
255
|
+
)
|
|
256
|
+
db["8751"] = _param(
|
|
257
|
+
"HC3 Room1 thermostat mode",
|
|
258
|
+
"1",
|
|
259
|
+
desc="Comfort",
|
|
260
|
+
data_type=1,
|
|
261
|
+
readonly=1,
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
db["1300"] = _inactive_param("HC3 Operating mode")
|
|
265
|
+
db["8002"] = _inactive_param("Status heating circuit 3")
|
|
266
|
+
|
|
267
|
+
return db
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# HTTP handlers
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class FakeBSBLAN:
|
|
276
|
+
"""Fake BSB-LAN HTTP server."""
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
num_circuits: int = 2,
|
|
281
|
+
passkey: str | None = None,
|
|
282
|
+
*,
|
|
283
|
+
omit_static_values: bool = False,
|
|
284
|
+
omit_dhw: bool = False,
|
|
285
|
+
firmware_version: str | None = None,
|
|
286
|
+
json_api_version: str = "2.0",
|
|
287
|
+
json_api_unavailable: bool = False,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Initialize fake server."""
|
|
290
|
+
self.db = build_parameter_db(num_circuits, omit_dhw=omit_dhw)
|
|
291
|
+
self.passkey = passkey
|
|
292
|
+
self.num_circuits = num_circuits
|
|
293
|
+
self.omit_static_values = omit_static_values
|
|
294
|
+
self.omit_dhw = omit_dhw
|
|
295
|
+
self.json_api_version = json_api_version
|
|
296
|
+
self.json_api_unavailable = json_api_unavailable
|
|
297
|
+
self.device_info = dict(DEVICE_INFO)
|
|
298
|
+
if firmware_version is not None:
|
|
299
|
+
self.device_info["version"] = firmware_version
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def omitted_parameters(self) -> set[str]:
|
|
303
|
+
"""Return parameter IDs that should be omitted from responses."""
|
|
304
|
+
omitted: set[str] = set()
|
|
305
|
+
|
|
306
|
+
if self.omit_static_values:
|
|
307
|
+
omitted.update({"714", "716", "1014", "1016", "1314", "1316"})
|
|
308
|
+
|
|
309
|
+
if self.omit_dhw:
|
|
310
|
+
# All DHW-related parameter IDs — omitting these causes the library
|
|
311
|
+
# to raise BSBLANError, matching real devices without hot water support
|
|
312
|
+
omitted.update(
|
|
313
|
+
{
|
|
314
|
+
"561",
|
|
315
|
+
"562",
|
|
316
|
+
"563",
|
|
317
|
+
"564",
|
|
318
|
+
"565",
|
|
319
|
+
"566",
|
|
320
|
+
"567",
|
|
321
|
+
"576",
|
|
322
|
+
"1600",
|
|
323
|
+
"1601",
|
|
324
|
+
"1610",
|
|
325
|
+
"1612",
|
|
326
|
+
"1614",
|
|
327
|
+
"1620",
|
|
328
|
+
"1630",
|
|
329
|
+
"1640",
|
|
330
|
+
"1641",
|
|
331
|
+
"1642",
|
|
332
|
+
"1644",
|
|
333
|
+
"1645",
|
|
334
|
+
"1646",
|
|
335
|
+
"1647",
|
|
336
|
+
"1648",
|
|
337
|
+
"1660",
|
|
338
|
+
"1661",
|
|
339
|
+
"1663",
|
|
340
|
+
"1680",
|
|
341
|
+
"8820",
|
|
342
|
+
"8830",
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return omitted
|
|
347
|
+
|
|
348
|
+
def _prefix(self) -> str:
|
|
349
|
+
"""Return URL prefix (with passkey if set)."""
|
|
350
|
+
if self.passkey:
|
|
351
|
+
return f"/{self.passkey}"
|
|
352
|
+
return ""
|
|
353
|
+
|
|
354
|
+
async def handle_jq(self, request: web.Request) -> web.Response:
|
|
355
|
+
"""Handle /JQ — query parameters."""
|
|
356
|
+
param_str = request.query.get("Parameter", "")
|
|
357
|
+
param_ids = [p.strip() for p in param_str.split(",") if p.strip()]
|
|
358
|
+
|
|
359
|
+
_LOGGER.info("JQ request: params=%s (from %s)", param_ids, request.remote)
|
|
360
|
+
|
|
361
|
+
result: dict[str, Any] = {}
|
|
362
|
+
for pid in param_ids:
|
|
363
|
+
if pid in self.omitted_parameters:
|
|
364
|
+
_LOGGER.info("Omitting parameter %s from response", pid)
|
|
365
|
+
continue
|
|
366
|
+
if pid in self.db:
|
|
367
|
+
result[pid] = self.db[pid]
|
|
368
|
+
else:
|
|
369
|
+
_LOGGER.warning("Unknown parameter requested: %s", pid)
|
|
370
|
+
result[pid] = _inactive_param(f"Unknown param {pid}")
|
|
371
|
+
|
|
372
|
+
return web.json_response(result)
|
|
373
|
+
|
|
374
|
+
async def handle_ji(self, request: web.Request) -> web.Response:
|
|
375
|
+
"""Handle /JI — device info."""
|
|
376
|
+
_LOGGER.info("JI request (device info) from %s", request.remote)
|
|
377
|
+
return web.json_response(self.device_info)
|
|
378
|
+
|
|
379
|
+
async def handle_js(self, request: web.Request) -> web.Response:
|
|
380
|
+
"""Handle /JS — set parameter."""
|
|
381
|
+
try:
|
|
382
|
+
body = await request.json()
|
|
383
|
+
except json.JSONDecodeError:
|
|
384
|
+
return web.json_response({"status": 2}, status=400)
|
|
385
|
+
|
|
386
|
+
param_id = str(body.get("Parameter", ""))
|
|
387
|
+
value = str(body.get("Value", ""))
|
|
388
|
+
|
|
389
|
+
_LOGGER.info("JS request: set param %s = %s", param_id, value)
|
|
390
|
+
|
|
391
|
+
if param_id in self.db:
|
|
392
|
+
self.db[param_id]["value"] = value
|
|
393
|
+
# Update desc for enum types
|
|
394
|
+
if self.db[param_id]["dataType"] == 1:
|
|
395
|
+
self.db[param_id]["desc"] = f"Set to {value}"
|
|
396
|
+
return web.json_response({"status": 1}) # 1 = success
|
|
397
|
+
|
|
398
|
+
_LOGGER.warning("JS: unknown parameter %s", param_id)
|
|
399
|
+
return web.json_response({"status": 2}) # 2 = error
|
|
400
|
+
|
|
401
|
+
async def handle_jv(self, request: web.Request) -> web.Response:
|
|
402
|
+
"""Handle /JV — JSON-API version."""
|
|
403
|
+
_LOGGER.info("JV request (json-api version) from %s", request.remote)
|
|
404
|
+
|
|
405
|
+
if self.json_api_unavailable:
|
|
406
|
+
_LOGGER.info("JV request simulated as unavailable (404)")
|
|
407
|
+
return web.Response(status=404)
|
|
408
|
+
|
|
409
|
+
return web.json_response({"api_version": self.json_api_version})
|
|
410
|
+
|
|
411
|
+
def create_app(self) -> web.Application:
|
|
412
|
+
"""Create the aiohttp application."""
|
|
413
|
+
app = web.Application()
|
|
414
|
+
prefix = self._prefix()
|
|
415
|
+
|
|
416
|
+
app.router.add_post(f"{prefix}/JQ", self.handle_jq)
|
|
417
|
+
app.router.add_post(f"{prefix}/JI", self.handle_ji)
|
|
418
|
+
app.router.add_post(f"{prefix}/JS", self.handle_js)
|
|
419
|
+
app.router.add_post(f"{prefix}/JV", self.handle_jv)
|
|
420
|
+
|
|
421
|
+
_LOGGER.info(
|
|
422
|
+
"Routes registered: %sJQ, %sJI, %sJS, %sJV",
|
|
423
|
+
prefix,
|
|
424
|
+
prefix,
|
|
425
|
+
prefix,
|
|
426
|
+
prefix,
|
|
427
|
+
)
|
|
428
|
+
return app
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def main() -> None:
|
|
432
|
+
"""Run the fake BSB-LAN server."""
|
|
433
|
+
parser = argparse.ArgumentParser(
|
|
434
|
+
description="Fake BSB-LAN server for testing multi-circuit config flows"
|
|
435
|
+
)
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"--port", type=int, default=8080, help="Port to listen on (default: 8080)"
|
|
438
|
+
)
|
|
439
|
+
parser.add_argument(
|
|
440
|
+
"--circuits",
|
|
441
|
+
type=int,
|
|
442
|
+
default=2,
|
|
443
|
+
choices=[1, 2, 3],
|
|
444
|
+
help="Number of active heating circuits (default: 2)",
|
|
445
|
+
)
|
|
446
|
+
parser.add_argument(
|
|
447
|
+
"--passkey", type=str, default=None, help="Passkey prefix (default: none)"
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
"--omit-static-values",
|
|
451
|
+
action="store_true",
|
|
452
|
+
help="Omit heating circuit min/max temperature parameters from responses",
|
|
453
|
+
)
|
|
454
|
+
parser.add_argument(
|
|
455
|
+
"--omit-dhw",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help="Omit all DHW (hot water) parameters, simulating a device without DHW",
|
|
458
|
+
)
|
|
459
|
+
parser.add_argument(
|
|
460
|
+
"--firmware-version",
|
|
461
|
+
type=str,
|
|
462
|
+
default=None,
|
|
463
|
+
help=(
|
|
464
|
+
"Override the reported firmware version (e.g. 1.0.0 to trigger the "
|
|
465
|
+
"outdated firmware repair issue)"
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
parser.add_argument(
|
|
469
|
+
"--json-api-version",
|
|
470
|
+
type=str,
|
|
471
|
+
default="2.0",
|
|
472
|
+
help="JSON-API version returned by /JV (default: 2.0)",
|
|
473
|
+
)
|
|
474
|
+
parser.add_argument(
|
|
475
|
+
"--json-api-unavailable",
|
|
476
|
+
action="store_true",
|
|
477
|
+
help="Simulate /JV not being retrievable (returns HTTP 404)",
|
|
478
|
+
)
|
|
479
|
+
args = parser.parse_args()
|
|
480
|
+
|
|
481
|
+
server = FakeBSBLAN(
|
|
482
|
+
num_circuits=args.circuits,
|
|
483
|
+
passkey=args.passkey,
|
|
484
|
+
omit_static_values=args.omit_static_values,
|
|
485
|
+
omit_dhw=args.omit_dhw,
|
|
486
|
+
firmware_version=args.firmware_version,
|
|
487
|
+
json_api_version=args.json_api_version,
|
|
488
|
+
json_api_unavailable=args.json_api_unavailable,
|
|
489
|
+
)
|
|
490
|
+
app = server.create_app()
|
|
491
|
+
|
|
492
|
+
print(f"\n{'=' * 60}")
|
|
493
|
+
print(" Fake BSB-LAN Server")
|
|
494
|
+
print(f" Circuits: {args.circuits} | Port: {args.port}")
|
|
495
|
+
print(f" MAC: {DEVICE_INFO['MAC']}")
|
|
496
|
+
if args.passkey:
|
|
497
|
+
print(f" Passkey: {args.passkey}")
|
|
498
|
+
if args.omit_static_values:
|
|
499
|
+
print(" Static values: omitted")
|
|
500
|
+
if args.omit_dhw:
|
|
501
|
+
print(" DHW (hot water): omitted")
|
|
502
|
+
print(f" Firmware: {server.device_info['version']}")
|
|
503
|
+
if args.json_api_unavailable:
|
|
504
|
+
print(" JSON-API (/JV): unavailable (simulated 404)")
|
|
505
|
+
else:
|
|
506
|
+
print(f" JSON-API (/JV): {server.json_api_version}")
|
|
507
|
+
print(f"{'=' * 60}")
|
|
508
|
+
print("\n Add integration in HA with:")
|
|
509
|
+
print(" Host: 127.0.0.1")
|
|
510
|
+
print(f" Port: {args.port}")
|
|
511
|
+
if args.passkey:
|
|
512
|
+
print(f" Passkey: {args.passkey}")
|
|
513
|
+
print(f"\n The config flow should detect {args.circuits} circuit(s)")
|
|
514
|
+
if args.circuits > 1:
|
|
515
|
+
print(" and show the heating circuit selection step.")
|
|
516
|
+
print()
|
|
517
|
+
|
|
518
|
+
web.run_app(app, host="127.0.0.1", port=args.port)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
if __name__ == "__main__":
|
|
522
|
+
main()
|