diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..54ce1fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,45 @@ + +## The problem + + + +## Environment + + +- Home Assistant Core release with the issue: +- Last working Home Assistant Core release (if known): +- Integration version causing this issue: + +## Problem-relevant `configuration dialog` + + +```yaml + +``` + + +## Traceback/Error logs + + +```txt + +``` + +## Additional information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3b83d88 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,60 @@ + +## Breaking change + + + +## Proposed change + + + +## Type of change + + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New integration (thank you!) +- [ ] New feature (which adds functionality to an existing integration) +- [ ] Deprecation (breaking change to happen in the future) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests + +## Additional information + + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: +- Link to documentation pull request: + +## Checklist + + +- [ ] The code change is tested and works locally. +- [ ] There is no commented out code in this PR. +- [ ] The code has been formatted using Ruff (`ruff format custom_components/victron`) diff --git a/.github/issue-label-bot.yaml b/.github/issue-label-bot.yaml new file mode 100644 index 0000000..0ac8bab --- /dev/null +++ b/.github/issue-label-bot.yaml @@ -0,0 +1,4 @@ +label-alias: + bug: 'bug' + feature_request: 'feature_request' + question: 'question' \ No newline at end of file diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..ae6e74b --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,105 @@ +--- +- name: "breaking-change" + color: ee0701 + description: "A breaking change for existing users." +- name: "bugfix" + color: ee0701 + description: "Inconsistencies or issues which will cause a problem for users or implementors." +- name: "documentation" + color: 0052cc + description: "Solely about the documentation of the project." +- name: "enhancement" + color: 1d76db + description: "Enhancement of the code, not introducing new features." +- name: "refactor" + color: 1d76db + description: "Improvement of existing code, not introducing new features." +- name: "performance" + color: 1d76db + description: "Improving performance, not introducing new features." +- name: "new-feature" + color: 0e8a16 + description: "New features or options." +- name: "maintenance" + color: 2af79e + description: "Generic maintenance tasks." +- name: "ci" + color: 1d76db + description: "Work that improves the continue integration." +- name: "dependencies" + color: 1d76db + description: "Upgrade or downgrade of project dependencies." + +- name: "in-progress" + color: fbca04 + description: "Issue is currently being resolved by a developer." +- name: "stale" + color: fef2c0 + description: "There has not been activity on this issue or PR for quite some time." +- name: "no-stale" + color: fef2c0 + description: "This issue or PR is exempted from the stable bot." + +- name: "security" + color: ee0701 + description: "Marks a security issue that needs to be resolved asap." +- name: "incomplete" + color: fef2c0 + description: "Marks a PR or issue that is missing information." +- name: "invalid" + color: fef2c0 + description: "Marks a PR or issue that is missing information." + +- name: "beginner-friendly" + color: 0e8a16 + description: "Good first issue for people wanting to contribute to the project." +- name: "help-wanted" + color: 0e8a16 + description: "We need some extra helping hands or expertise in order to resolve this." + +- name: "priority-critical" + color: ee0701 + description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." +- name: "priority-high" + color: b60205 + description: "After critical issues are fixed, these should be dealt with before any further issues." +- name: "priority-medium" + color: 0e8a16 + description: "This issue may be useful, and needs some attention." +- name: "priority-low" + color: e4ea8a + description: "Nice addition, maybe... someday..." + +- name: "major" + color: b60205 + description: "This PR causes a major version bump in the version number." +- name: "minor" + color: 0e8a16 + description: "This PR causes a minor version bump in the version number." + + +- name: "shipped" + color: 006b75 + description: "This issue has been shipped. 🚱" +- name: "on-hold" + color: 0052cc + description: "This issue is on hold and will not be worked on until further notice." +- name: "duplicate" + color: fef2c0 + description: "This reported issue already exists." +- name: "wontfix" + color: ffffff + description: "This issue will not be fixed." + +- name: "connection stability" + color: 32AE82 + description: "Issue related to the stability of the connection." +- name: "data accuracy" + color: 32AE82 + description: "Issue related to the format or representation of returned/to be returned data." +- name: "specsheet mismatch" + color: d93f0b + description: "Issue related to the mismatch between the victron specsheet and the actual implementation." +- name: "shipping next release (do not close)🚱" + color: b60205 + description: "This issue is scheduled to be shipped in the next release." \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..2a64875 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,85 @@ +--- +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +sort-direction: ascending + +categories: + - title: "🚹 Breaking changes" + labels: + - "breaking-change" + - title: "🐛 Bug fixes" + labels: + - "bugfix" + - title: "✹ New features" + labels: + - "new-feature" + - title: "🚀 Enhancements" + collapse-after: 4 + labels: + - "enhancement" + - "refactor" + - "performance" + - title: "🧰 Maintenance" + collapse-after: 4 + labels: + - "maintenance" + - "ci" + - title: "📚 Documentation" + collapse-after: 4 + labels: + - "documentation" + - title: "âŹ†ïž Dependency updates" + collapse-after: 4 + labels: + - "dependencies" + +version-resolver: + major: + labels: + - "major" + - "breaking-change" + minor: + labels: + - "minor" + - "new-feature" + patch: + labels: + - "bugfix" + - "chore" + - "ci" + - "decoding" + - "dependencies" + - "documentation" + - "enhancement" + - "performance" + - "refactor" + default: patch + +exclude-contributors: + - sfstar + +template: | + ## Changes + + $CHANGES + + Special thanks to $CONTRIBUTORS for this release! + + +autolabeler: + - label: 'chore' + files: + - '*.md' + branch: + - '/documentation\/.+/' + - label: 'bug' + branch: + - '/bugfix\/.+/' + title: + - '/bugfix/i' + - label: 'new-feature' + branch: + - '/feature\/.+/' + body: + - '/adds/' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..ad0674e --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,38 @@ +on: + release: + types: [published] + +jobs: + build: + name: 🚀 Release + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Get version + id: version + uses: home-assistant/actions/helpers/version@main + + - name: 🔱 Adjust version number + run: | + sed -i 's/v0.0.0/${{ steps.version.outputs.version }}/' custom_components/victron/manifest.json + + - name: 📩 Created zipped release package + shell: bash + run: | + cd "${{ github.workspace }}/custom_components/victron" + zip victron.zip ./* -x '.*' + + - name: 🔏 Sign release package + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: ${{ github.workspace }}/custom_components/victron/victron.zip + + - name: âŹ†ïž Upload zip to release + uses: softprops/action-gh-release@v2.2.1 + with: + files: ${{ github.workspace }}/custom_components/victron/victron.zip \ No newline at end of file diff --git a/.github/workflows/hacs.yml b/.github/workflows/hacs.yml index ea6cf6f..a9b8265 100644 --- a/.github/workflows/hacs.yml +++ b/.github/workflows/hacs.yml @@ -1,7 +1,6 @@ name: HACS Action on: - push: pull_request: schedule: - cron: "0 0 * * *" diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml index 435962d..b94d633 100644 --- a/.github/workflows/hassfest.yml +++ b/.github/workflows/hassfest.yml @@ -1,7 +1,6 @@ name: Validate with hassfest on: - push: pull_request: schedule: - cron: '0 0 * * *' diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml new file mode 100644 index 0000000..b4f51e9 --- /dev/null +++ b/.github/workflows/labels.yaml @@ -0,0 +1,24 @@ +--- +name: Sync labels + +# yamllint disable-line rule:truthy +on: + push: + branches: + - main + paths: + - .github/labels.yml + workflow_dispatch: + +jobs: + labels: + name: ♻ Sync labels + runs-on: ubuntu-latest + steps: + - name: — Check out code from GitHub + uses: actions/checkout@v4 + + - name: 🚀 Run Label Syncer + uses: micnncim/action-label-syncer@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml new file mode 100644 index 0000000..ddf21fb --- /dev/null +++ b/.github/workflows/pr-labels.yaml @@ -0,0 +1,27 @@ +--- +name: PR Labels + +# yamllint disable-line rule:truthy +on: + pull_request_target: + types: + - opened + - labeled + - unlabeled + - synchronize + workflow_call: + +jobs: + pr_labels: + name: Verify + runs-on: ubuntu-latest + steps: + - name: đŸ· Verify PR has a valid label + uses: jesusvasquez333/verify-pr-label-action@v1.4.0 + with: + pull-request-number: "${{ github.event.pull_request.number }}" + github-token: "${{ secrets.GITHUB_TOKEN }}" + valid-labels: >- + breaking-change, bugfix, documentation, enhancement, + refactor, performance, new-feature, maintenance, ci, dependencies + disable-reviews: true \ No newline at end of file diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..2a93b21 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,22 @@ +--- +name: Release Drafter + +# yamllint disable-line rule:truthy +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + name: ✏ Draft release + runs-on: ubuntu-latest + steps: + - name: 🚀 Run Release Drafter + uses: release-drafter/release-drafter@v6.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml new file mode 100644 index 0000000..0de43a0 --- /dev/null +++ b/.github/workflows/ruff.yaml @@ -0,0 +1,9 @@ +name: Ruff + +on: [ pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 \ No newline at end of file diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..ee61bf4 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,18 @@ +name: "Close stale issues/pull requests" +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days' + stale-pr-message: 'This pull request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days' + days-before-stale: 60 + days-before-close: 30 + operations-per-run: 500 + exempt-issue-labels: 'on-hold' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be9e74..6ded410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ custom_components/victron/__pycache__ .DS_Store +.idea \ No newline at end of file diff --git a/README.md b/README.md index 5ca7d39..3b98ada 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ As of november 2022 the built-in home assistant modbus integration runs on a ver If you install this integration the built-in modbus integration will stop to work due to breaking changes between 2.x.x and 3.0.0 ## Important announcement: -Starting from homeassistant core version 2023.2.x the built-in modbus integration now uses pymodbus version 3.1.1. -Version 0.0.7 (and up) of this integration will also use the 3.1.1 pymodbus version. +Starting from homeassistant core version 2025.1.x the built-in modbus integration now uses pymodbus version 3.7.4. +Version 0.4.0 (and up) of this integration will also use the 3.7.4 pymodbus version. Although core version >= 2023.2 and previous versions of this integration should be compatible it is recommended that all users update both core and this integration in the same patch round. Since having multiple library version requirements might cause the built-in 3.1.1 library to be overwritten by 3.0.2 reference of versions 0.0.6 and earlier. @@ -68,7 +68,7 @@ This could cause issues if you are using specific configuration options of the b ## HACS ### Default -1. Add the integration through this link: +1. Add the integration through this link: [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=sfstar&repository=hass-victron&category=integration) 3. Restart home assistant 4. Setup integration via the integration page. @@ -87,7 +87,7 @@ Although this integration speaks to the (exposed by victron) modbusTCP server it - (when implemented) writing to write_registers (for example changing the ess setpoint value) Therefore the following applies to anyone using this code: -- This code is provided as is. +- This code is provided as is. - The developer does not assume any liability for issues caused by the use of this integration. - Use at your own risk. @@ -137,7 +137,7 @@ The voltage profile to use in order to calculate the voltage boundaries (i.e. 4s The AC voltage for a single phase in your region (currently supported is US: 120v and EU: 230v) This setting is used in combination with AC current to automatically calcultate the max wattage for applicable wattage settings. -# Resources +# Resources The following links can be helpfull resources: - [setting up modbusTCP on the gx device](https://www.victronenergy.com/live/ccgx:modbustcp_faq) - [Great UI card for the gx device data](https://github.com/flyrmyr/system-flow-card) diff --git a/custom_components/victron/base.py b/custom_components/victron/base.py index f5d741e..04722b9 100644 --- a/custom_components/victron/base.py +++ b/custom_components/victron/base.py @@ -1,3 +1,5 @@ +"""Module defines entity descriptions for Victron components.""" + from collections.abc import Callable from dataclasses import dataclass @@ -7,8 +9,11 @@ @dataclass class VictronBaseEntityDescription(EntityDescription): + """An extension of EntityDescription for Victron components.""" + @staticmethod def lambda_func(): + """Return an entitydescription.""" return lambda data, slave, key: data["data"][str(slave) + "." + str(key)] slave: int = None @@ -17,4 +22,6 @@ def lambda_func(): @dataclass class VictronWriteBaseEntityDescription(VictronBaseEntityDescription): + """An extension of VictronBaseEntityDescription for writeable Victron components.""" + address: int = None diff --git a/custom_components/victron/binary_sensor.py b/custom_components/victron/binary_sensor.py index dfaf629..0cf2de8 100644 --- a/custom_components/victron/binary_sensor.py +++ b/custom_components/victron/binary_sensor.py @@ -43,11 +43,9 @@ async def async_setup_entry( for name in registerLedger: for register_name, registerInfo in register_info_dict[name].items(): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo", + slave, + registerLedger, ) if isinstance(registerInfo.entityType, BoolReadEntityType): @@ -56,7 +54,7 @@ async def async_setup_entry( name=register_name.replace("_", " "), slave=slave, ) - _LOGGER.debug("composed description == " + str(description)) + _LOGGER.debug("composed description == %s", description) descriptions.append(description) entities = [] @@ -112,6 +110,7 @@ def is_on(self) -> bool: @property def available(self) -> bool: + """Return True if entity is available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/button.py b/custom_components/victron/button.py index b590b2d..84d71fc 100644 --- a/custom_components/victron/button.py +++ b/custom_components/victron/button.py @@ -41,11 +41,9 @@ async def async_setup_entry( for name in registerLedger: for register_name, registerInfo in register_info_dict[name].items(): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo", + slave, + registerLedger, ) if not config_entry.options[CONF_ADVANCED_OPTIONS]: continue @@ -58,7 +56,7 @@ async def async_setup_entry( device_class=ButtonDeviceClass.RESTART, address=registerInfo.register, ) - _LOGGER.debug("composed description == " + str(description)) + _LOGGER.debug("composed description == %s", description) descriptions.append(description) entities = [] @@ -109,6 +107,7 @@ async def async_press(self) -> None: @property def available(self) -> bool: + """Return True if entity available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/config_flow.py b/custom_components/victron/config_flow.py index 1131a07..0eabab3 100644 --- a/custom_components/victron/config_flow.py +++ b/custom_components/victron/config_flow.py @@ -65,8 +65,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, # your_validate_func, data["username"], data["password"] # ) - _LOGGER.debug("host = " + data[CONF_HOST]) - _LOGGER.debug("port = " + str(data[CONF_PORT])) + _LOGGER.debug("host = %s", data[CONF_HOST]) + _LOGGER.debug("port = %s", data[CONF_PORT]) hub = VictronHub(data[CONF_HOST], data[CONF_PORT]) try: @@ -75,11 +75,12 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, discovered_devices = await scan_connected_devices(hub=hub) _LOGGER.debug("successfully discovered devices") except HomeAssistantError: - _LOGGER.error("failed to connect to the victron device:") + _LOGGER.error("Failed to connect to the victron device:") return {"title": DOMAIN, "data": discovered_devices} async def scan_connected_devices(hub: VictronHub) -> list: + """Scan for connected devices.""" return hub.determine_present_devices() @@ -90,6 +91,7 @@ class VictronFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 def __init__(self): + """Initialize the config flow.""" self.advanced_options = None self.interval = None self.port = None @@ -282,7 +284,10 @@ async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) class parsedEntry: + """Parsed entry.""" + def __init__(self, decoderInfo: RegisterInfo, value): + """Initialize the parsed entry.""" self.decoderInfo = decoderInfo self.value = value @@ -398,6 +403,7 @@ async def async_step_init( return None def init_read_form(self, errors: dict): + """Handle read support and limit settings if requested.""" return self.async_show_form( step_id="init_read", errors=errors, @@ -413,6 +419,7 @@ def init_read_form(self, errors: dict): ) def init_write_form(self, errors: dict): + """Handle write support and limit settings if requested.""" config = dict(self.config_entry.options) system_ac_voltage_default = self.config_entry.options.get( CONF_AC_SYSTEM_VOLTAGE, AC_VOLTAGES["US (120)"] @@ -494,6 +501,7 @@ def init_write_form(self, errors: dict): @staticmethod def get_dict_key(dict, val): + """Get the key from a dictionary.""" for key, value in dict.items(): if val == value: return key diff --git a/custom_components/victron/const.py b/custom_components/victron/const.py index 71d0fa2..e6c3ea3 100644 --- a/custom_components/victron/const.py +++ b/custom_components/victron/const.py @@ -22,6 +22,8 @@ class DeviceType(Enum): + """Enum for device types.""" + GRID = 1 TANK = 2 MULTI = 3 @@ -55,7 +57,10 @@ class DeviceType(Enum): class STRING: + """Class for string data type.""" + def __init__(self, length=1, read_length=None): + """Initialize the string data type.""" self.length = length self.readLength = read_length if read_length is not None else length * 2 @@ -70,50 +75,76 @@ def __init__(self, length=1, read_length=None): class EntityType: + """Base entityType.""" + def __init__(self, entityTypeName) -> None: + """Initialize the entity type.""" self.entityTypeName = entityTypeName class ReadEntityType(EntityType): + """Read entity type.""" + def __init__(self, entityTypeName: str = "read") -> None: + """Initialize the read entity type.""" super().__init__(entityTypeName=entityTypeName) class TextReadEntityType(ReadEntityType): + """Text read entity type.""" + def __init__(self, decodeEnum: Enum) -> None: + """Initialize the text read entity type.""" super().__init__() self.decodeEnum = decodeEnum class BoolReadEntityType(ReadEntityType): + """Bool read entity type.""" + def __init__(self) -> None: + """Initialize the bool read entity type.""" super().__init__(entityTypeName="bool") class ButtonWriteType(EntityType): + """Button write type.""" + def __init__(self) -> None: + """Initialize the button write type.""" super().__init__(entityTypeName="button") class SwitchWriteType(EntityType): + """Switch write type.""" + def __init__(self) -> None: + """Initialize the switch write type.""" super().__init__(entityTypeName="switch") class SliderWriteType(EntityType): + """Slider write type.""" + def __init__(self, powerType="", negative: bool = False) -> None: + """Initialize the slider write type.""" super().__init__(entityTypeName="slider") self.powerType = powerType self.negative = negative class SelectWriteType(EntityType): + """Select write type.""" + def __init__(self, optionsEnum: Enum) -> None: + """Initialize the select write type.""" super().__init__(entityTypeName="select") self.options = optionsEnum class RegisterInfo: + """Class for register information.""" + def __init__( self, register, @@ -123,6 +154,7 @@ def __init__( entityType: EntityType = ReadEntityType(), step=0, ) -> None: + """Initialize the register info.""" self.register = register self.dataType = dataType self.unit = ( @@ -137,6 +169,7 @@ def __init__( self.entityType = entityType def determine_stateclass(self): + """Determine the state class.""" if self.unit == UnitOfEnergy.KILO_WATT_HOUR: return SensorStateClass.TOTAL_INCREASING if self.unit is None: @@ -145,6 +178,8 @@ def determine_stateclass(self): class generic_alarm_ledger(Enum): + """Generic alarm ledger.""" + OK = 0 WARNING = 1 ALARM = 2 @@ -207,6 +242,8 @@ class generic_alarm_ledger(Enum): class vebus_mode(Enum): + """Vebus mode.""" + CHARGER = 1 INVERTER = 2 ON = 3 @@ -214,12 +251,16 @@ class vebus_mode(Enum): class generic_activeinput(Enum): + """Generic active input.""" + AC_INPUT_1 = 0 AC_INPUT_2 = 1 DISCONNECTED = 240 class generic_charger_state(Enum): + """Generic charger state.""" + OFF = 0 LOW_POWER = 1 FAULT = 2 @@ -236,6 +277,8 @@ class generic_charger_state(Enum): class vebus_error(Enum): + """Vebus error.""" + OK = 0 EXTERNAL_PHASE_TRIGGERED_SWITCHOFF = 1 MK2_TYPE_MISMATCH = 2 @@ -693,6 +736,8 @@ class vebus_error(Enum): class battery_state(Enum): + """Battery state.""" + WAIT_START_INIT = 0 BEFORE_BOOT_INIT = 1 BEFORE_BOOT_DELAY_INIT = 2 @@ -713,6 +758,8 @@ class battery_state(Enum): class battery_error(Enum): + """Battery error.""" + NONE = 0 BATTERY_INIT_ERROR = 1 NO_BATTERIES_CONNECTED = 2 @@ -814,11 +861,15 @@ class battery_error(Enum): class solarcharger_mode(Enum): + """Solar charger mode.""" + ON = 1 OFF = 4 class solarcharger_state(Enum): + """Solar charger state.""" + OFF = 0 FAULT = 2 BULK = 3 @@ -832,6 +883,8 @@ class solarcharger_state(Enum): class solarcharger_equalization_pending(Enum): + """Solar charger equalization pending.""" + NO = 0 YES = 1 ERROR = 2 @@ -839,6 +892,8 @@ class solarcharger_equalization_pending(Enum): class generic_charger_errorcode(Enum): + """Generic charger error code.""" + NONE = 0 TEMPERATURE_HIGH = 1 VOLTAGE_HIGH = 2 @@ -859,6 +914,8 @@ class generic_charger_errorcode(Enum): class generic_mppoperationmode(Enum): + """Generic MPP operation mode.""" + OFF = 0 LIMITED = 1 ACTIVE = 2 @@ -1009,6 +1066,8 @@ class generic_mppoperationmode(Enum): class generic_position(Enum): + """Generic position.""" + AC_INPUT_1 = 0 AC_OUTPUT = 1 AC_INPUT_2 = 2 @@ -1083,6 +1142,8 @@ class generic_position(Enum): class charger_mode(Enum): + """Charger mode.""" + OFF = 0 ON = 1 ERROR = 2 @@ -1207,6 +1268,8 @@ class charger_mode(Enum): class ess_batterylife_state(Enum): + """ESS battery life state.""" + BL_DISABLED_DUPLICATE_1 = 0 RESTARTING = 1 SELF_CONSUMPTION = 2 @@ -1223,6 +1286,8 @@ class ess_batterylife_state(Enum): class ess_mode(Enum): + """ESS mode.""" + SELF_CONSUMPTION_WITH_BATTERY_LIFE = 0 SELF_CONSUMPTION = 1 KEEP_CHARGED = 2 @@ -1246,6 +1311,8 @@ class ess_mode(Enum): class tank_fluidtype(Enum): + """Tank fluid type.""" + FUEL = 0 FRESH_WATER = 1 WASTE_WATER = 2 @@ -1261,6 +1328,8 @@ class tank_fluidtype(Enum): class tank_status(Enum): + """Tank status.""" + OK = 0 DISCONNECTED = 1 SHORT_CIRCUITED = 2 @@ -1346,6 +1415,8 @@ class tank_status(Enum): class inverter_mode(Enum): + """Inverter mode.""" + ON = 2 OFF = 4 ECO = 5 @@ -1452,6 +1523,8 @@ class inverter_mode(Enum): class genset_status(Enum): + """Genset status.""" + STANDBY = 0 STARTUP_1 = 1 STARTUP_2 = 2 @@ -1466,6 +1539,8 @@ class genset_status(Enum): class genset_errorcode(Enum): + """Genset error code.""" + NONE = 0 AC_L1_VOLTAGE_TOO_LOW = 1 AC_L1_FREQUENCY_TOO_LOW = 2 @@ -1612,12 +1687,16 @@ class genset_errorcode(Enum): class temperature_type(Enum): + """Temperature type.""" + BATTERY = 0 FRIDGE = 1 GENERIC = 2 class temperature_status(Enum): + """Temperature status.""" + OK = 0 DISCONNECTED = 1 SHORT_CIRCUITED = 2 @@ -1655,6 +1734,8 @@ class temperature_status(Enum): class digitalinput_state(Enum): + """Digital input state.""" + LOW = 0 HIGH = 1 OFF = 2 @@ -1670,6 +1751,8 @@ class digitalinput_state(Enum): class digitalinput_type(Enum): + """Digital input type.""" + DOOR = 2 BILGE_PUMP = 3 BILGE_ALARM = 4 @@ -1698,6 +1781,8 @@ class digitalinput_type(Enum): class generator_runningbyconditioncode(Enum): + """Generator running by condition code.""" + STOPPED = 0 MANUAL = 1 TEST_RUN = 2 @@ -1712,12 +1797,16 @@ class generator_runningbyconditioncode(Enum): class generator_state(Enum): + """Generator state.""" + STOPPED = 0 RUNNING = 1 ERROR = 10 class generator_error(Enum): + """Generator error.""" + NONE = 0 REMOTE_DISABLED = 1 REMOTE_FAULT = 2 @@ -1769,12 +1858,16 @@ class generator_error(Enum): class evcharger_mode(Enum): + """EV charger mode.""" + AC_INPUT_1 = 0 AC_OUTPUT = 1 AC_INPUT_2 = 2 class evcharger_status(Enum): + """EV charger status.""" + DISCONNECTED = 0 CONNECTED = 1 CHARGING = 2 @@ -1901,6 +1994,8 @@ class evcharger_status(Enum): class alternator_state(Enum): + """Alternator state.""" + OFF = 0 FAULT = 2 BULK = 3 @@ -1912,6 +2007,8 @@ class alternator_state(Enum): class alternator_errorcode(Enum): + """Alternator error code.""" + HIGH_BATTERY_TEMPERATURE = 12 HIGH_BATTERY_VOLTAGE = 13 LOW_BATTERY_VOLTAGE = 14 @@ -2145,6 +2242,8 @@ class alternator_errorcode(Enum): class multi_mode(Enum): + """Multi mode.""" + CHARGER = 1 INVERTER = 2 ON = 3 @@ -2152,6 +2251,8 @@ class multi_mode(Enum): class multi_input_type(Enum): + """Multi input type.""" + UNUSED = 0 GRID = 1 GENSET = 2 @@ -2418,6 +2519,8 @@ class multi_input_type(Enum): class register_input_source(Enum): + """Input source.""" + UNKNOWN = 0 GRID = 1 GENERATOR = 2 @@ -2460,6 +2563,8 @@ class register_input_source(Enum): class system_battery_state(Enum): + """Battery state.""" + IDLE = 0 CHARGING = 1 DISCHARGING = 2 diff --git a/custom_components/victron/coordinator.py b/custom_components/victron/coordinator.py index e1d8019..21324ef 100644 --- a/custom_components/victron/coordinator.py +++ b/custom_components/victron/coordinator.py @@ -1,14 +1,15 @@ +"""Define the Victron Energy Device Update Coordinator.""" + from __future__ import annotations from collections import OrderedDict from datetime import timedelta import logging +import pymodbus from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadDecoder -import pymodbus - if "3.7.0" <= pymodbus.__version__ <= "3.7.4": from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse else: @@ -86,7 +87,7 @@ async def _async_update_data(self) -> dict: unavailable_entities[full_key] = False _LOGGER.warning( - "no valid data returned for entities of slave: %s (if the device continues to no longer update) check if the device was physically removed. Before opening an issue please force a rescan to attempt to resolve this issue", + "No valid data returned for entities of slave: %s (if the device continues to no longer update) check if the device was physically removed. Before opening an issue please force a rescan to attempt to resolve this issue", unit, ) else: @@ -114,6 +115,7 @@ def parse_register_data( registerInfo: OrderedDict(str, RegisterInfo), unit: int, ) -> dict: + """Parse the register data.""" decoder = BinaryPayloadDecoder.fromRegisters( buffer.registers, byteorder=Endian.BIG ) @@ -149,11 +151,13 @@ def parse_register_data( return decoded_data def decode_scaling(self, number, scale, unit): + """Decode the scaling.""" if unit == "" and scale == 1: return round(number) return number / scale def encode_scaling(self, value, unit, scale): + """Encode the scaling.""" if scale == 0: return value if unit == "" and scale == 1: @@ -161,19 +165,23 @@ def encode_scaling(self, value, unit, scale): return int(value * scale) def get_data(self): + """Return the data.""" return self.data async def async_update_local_entry(self, key, value): + """Update the local entry.""" data = self.data data["data"][key] = value self.async_set_updated_data(data) - """Force update data after change.""" + await self.async_request_refresh() def processed_data(self): + """Return the processed data.""" return self.data async def fetch_registers(self, unit, registerData): + """Fetch the registers.""" try: # run api_update in async job return await self.hass.async_add_executor_job( @@ -184,6 +192,7 @@ async def fetch_registers(self, unit, registerData): raise UpdateFailed("Fetching registers failed") from e def write_register(self, unit, address, value): + """Write to the register.""" # try: self.api_write(unit, address, value) @@ -193,10 +202,12 @@ def write_register(self, unit, address, value): # _LOGGER.error("failed to write to option:", e def api_write(self, unit, address, value): + """Write to the api.""" # recycle connection return self.api.write_register(unit=unit, address=address, value=value) def api_update(self, unit, registerInfo): + """Update the api.""" # recycle connection return self.api.read_holding_registers( unit=unit, @@ -206,10 +217,13 @@ def api_update(self, unit, registerInfo): class DecodeDataTypeUnsupported(Exception): - pass + """Exception for unsupported data type.""" class DataEntry: + """Data entry class.""" + def __init__(self, unit, value) -> None: + """Initialize the data entry.""" self.unit = unit self.value = value diff --git a/custom_components/victron/hub.py b/custom_components/victron/hub.py index eeed9e4..64f9dd5 100644 --- a/custom_components/victron/hub.py +++ b/custom_components/victron/hub.py @@ -1,3 +1,5 @@ +"""Support for Victron Energy devices.""" + from collections import OrderedDict import logging import threading @@ -12,6 +14,8 @@ class VictronHub: + """Victron Hub.""" + def __init__(self, host: str, port: int) -> None: """Initialize.""" self.host = host @@ -20,17 +24,21 @@ def __init__(self, host: str, port: int) -> None: self._lock = threading.Lock() def is_still_connected(self): + """Check if the connection is still open.""" return self._client.is_socket_open() def connect(self): + """Connect to the Modbus TCP server.""" return self._client.connect() def disconnect(self): + """Disconnect from the Modbus TCP server.""" if self._client.is_socket_open(): return self._client.close() return None def write_register(self, unit, address, value): + """Write a register.""" slave = int(unit) if unit else 1 return self._client.write_register(address=address, value=value, slave=slave) @@ -42,6 +50,7 @@ def read_holding_registers(self, unit, address, count): ) def calculate_register_count(self, registerInfoDict: OrderedDict): + """Calculate the number of registers to read.""" first_key = next(iter(registerInfoDict)) last_key = next(reversed(registerInfoDict)) end_correction = 1 @@ -55,10 +64,12 @@ def calculate_register_count(self, registerInfoDict: OrderedDict): ) + end_correction def get_first_register_id(self, registerInfoDict: OrderedDict): + """Return first register id.""" first_register = next(iter(registerInfoDict)) return registerInfoDict[first_register].register def determine_present_devices(self): + """Determine which devices are present.""" valid_devices = {} for unit in valid_unit_ids: @@ -74,12 +85,10 @@ def determine_present_devices(self): result = self.read_holding_registers(unit, address, count) if result.isError(): _LOGGER.debug( - "result is error for unit: " - + str(unit) - + " address: " - + str(address) - + " count: " - + str(count) + "result is error for unit: %s address: %s count: %s", + unit, + address, + count, ) else: working_registers.append(key) @@ -89,6 +98,6 @@ def determine_present_devices(self): if len(working_registers) > 0: valid_devices[unit] = working_registers else: - _LOGGER.debug("no registers found for unit: " + str(unit)) + _LOGGER.debug("no registers found for unit: %s", unit) return valid_devices diff --git a/custom_components/victron/manifest.json b/custom_components/victron/manifest.json index e0217bb..63a96f9 100644 --- a/custom_components/victron/manifest.json +++ b/custom_components/victron/manifest.json @@ -15,6 +15,6 @@ "pymodbus>=3.7.4" ], "ssdp": [], - "version": "v0.4.0", + "version": "v0.0.0", "zeroconf": [] } diff --git a/custom_components/victron/number.py b/custom_components/victron/number.py index fbbb315..c489fd5 100644 --- a/custom_components/victron/number.py +++ b/custom_components/victron/number.py @@ -61,11 +61,9 @@ async def async_setup_entry( for name in registerLedger: for register_name, registerInfo in register_info_dict[name].items(): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo", + slave, + registerLedger, ) if isinstance(registerInfo.entityType, SliderWriteType): @@ -93,7 +91,7 @@ async def async_setup_entry( scale=registerInfo.scale, native_step=registerInfo.step, ) - _LOGGER.debug("composed description == " + str(descriptions)) + _LOGGER.debug("composed description == %s", descriptions) descriptions.append(description) entities = [] @@ -109,6 +107,7 @@ async def async_setup_entry( def determine_min_value( unit, config_entry: config_entries.ConfigEntry, powerType, negative: bool ) -> int: + """Determine the minimum value for a number entity.""" if unit == PERCENTAGE: return 0 if unit == UnitOfElectricPotential.VOLT: @@ -148,6 +147,7 @@ def determine_min_value( def determine_max_value( unit, config_entry: config_entries.ConfigEntry, powerType ) -> int: + """Determine the maximum value for a number entity.""" if unit == PERCENTAGE: return 100 if unit == UnitOfElectricPotential.VOLT: @@ -188,6 +188,8 @@ class VictronNumberMixin: @dataclass class VictronNumberStep: + """A class that adds stepping to number entities.""" + native_step: float = 0 @@ -198,9 +200,10 @@ class VictronEntityDescription( VictronNumberMixin, VictronNumberStep, ): + """Describes victron number entity.""" + # Overwrite base entitydescription property to resolve automatic property ordering issues when a mix of non-default and default properties are used key: str | None = None - """Describes victron number entity.""" class VictronNumber(NumberEntity): @@ -266,6 +269,7 @@ def native_value(self) -> float: @property def native_step(self) -> float | None: + """Return the step width of the entity.""" if ( self.description.mode != NumberMode.SLIDER ): # allow users to skip stepping in case of box mode @@ -284,14 +288,17 @@ def native_step(self) -> float | None: @property def native_min_value(self) -> float: + """Return the minimum value of the entity.""" return self.description.native_min_value @property def native_max_value(self) -> float: + """Return the maximum value of the entity.""" return self.description.native_max_value @property def available(self) -> bool: + """Return True if entity is available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/select.py b/custom_components/victron/select.py index 056c464..1b990be 100644 --- a/custom_components/victron/select.py +++ b/custom_components/victron/select.py @@ -45,11 +45,9 @@ async def async_setup_entry( for register_name, registerInfo in register_info_dict[name].items(): if isinstance(registerInfo.entityType, SelectWriteType): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo", + slave, + registerLedger, ) description = VictronEntityDescription( @@ -61,7 +59,7 @@ async def async_setup_entry( ) descriptions.append(description) - _LOGGER.debug("composed description == " + str(description)) + _LOGGER.debug("composed description == %s", description) entities = [] entity = {} @@ -93,6 +91,7 @@ def __init__( coordinator: victronEnergyDeviceUpdateCoordinator, description: VictronEntityDescription, ) -> None: + """Initialize the select.""" self._attr_native_value = None _LOGGER.debug("select init") self.coordinator = coordinator @@ -138,6 +137,7 @@ async def async_update(self) -> None: @property def current_option(self) -> str: + """Return the currently selected option.""" return self.description.options( self.description.value_fn( self.coordinator.processed_data(), @@ -148,6 +148,7 @@ def current_option(self) -> str: @property def options(self) -> list: + """Return a list of available options.""" return [option.name for option in self.description.options] async def async_select_option(self, option: str) -> None: @@ -163,6 +164,7 @@ async def async_select_option(self, option: str) -> None: # TODO extract these type of property definitions to base class @property def available(self) -> bool: + """Return True if entity available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/sensor.py b/custom_components/victron/sensor.py index c0853f5..c349e3d 100644 --- a/custom_components/victron/sensor.py +++ b/custom_components/victron/sensor.py @@ -61,11 +61,9 @@ async def async_setup_entry( for name in registerLedger: for register_name, registerInfo in register_info_dict[name].items(): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo", + slave, + registerLedger, ) if config_entry.options[CONF_ADVANCED_OPTIONS]: if not isinstance( @@ -86,7 +84,7 @@ async def async_setup_entry( if isinstance(registerInfo.entityType, TextReadEntityType) else None, ) - _LOGGER.debug("composed description == " + str(description)) + _LOGGER.debug("composed description == %s", description) descriptions.append(description) @@ -101,6 +99,7 @@ async def async_setup_entry( def determine_victron_device_class(name, unit): + """Determine the device class of a sensor based on its name and unit.""" if unit == PERCENTAGE and "soc" in name: return SensorDeviceClass.BATTERY if unit == PERCENTAGE: @@ -205,6 +204,7 @@ def _handle_coordinator_update(self) -> None: @property def available(self) -> bool: + """Return True if entity is available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/switch.py b/custom_components/victron/switch.py index a2cd59e..2e496d5 100644 --- a/custom_components/victron/switch.py +++ b/custom_components/victron/switch.py @@ -42,11 +42,10 @@ async def async_setup_entry( for name in registerLedger: for register_name, registerInfo in register_info_dict[name].items(): _LOGGER.debug( - "unit == " - + str(slave) - + " registerLedger == " - + str(registerLedger) - + " registerInfo " + "unit == %s registerLedger == %s registerInfo == %s", + slave, + registerLedger, + registerInfo, ) if isinstance(registerInfo.entityType, SwitchWriteType): @@ -57,7 +56,7 @@ async def async_setup_entry( address=registerInfo.register, ) descriptions.append(description) - _LOGGER.debug("composed description == " + str(description)) + _LOGGER.debug("composed description == %s", description) entities = [] entity = {} @@ -85,6 +84,7 @@ def __init__( coordinator: victronEnergyDeviceUpdateCoordinator, description: VictronEntityDescription, ) -> None: + """Initialize the switch.""" self.coordinator = coordinator self.description: VictronEntityDescription = description self._attr_name = f"{description.name}" @@ -118,16 +118,17 @@ async def async_turn_off(self, **kwargs: Any) -> None: @property def is_on(self) -> bool: + """Return true if switch is on.""" data = self.description.value_fn( self.coordinator.processed_data(), self.description.slave, self.description.key, ) - """Return true if switch is on.""" return cast(bool, data) @property def available(self) -> bool: + """Return True if entity is available.""" full_key = str(self.description.slave) + "." + self.description.key return self.coordinator.processed_data()["availability"][full_key] diff --git a/custom_components/victron/translations/de.json b/custom_components/victron/translations/de.json new file mode 100644 index 0000000..23f467d --- /dev/null +++ b/custom_components/victron/translations/de.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "GerĂ€t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "UngĂŒltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "already_configured": "Nur eine Instanz der Victron-Integration wird derzeit unterstĂŒtzt" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "interval": "Aktualisierungsintervall in (s)", + "advanced": "Aktiviert SchreibunterstĂŒtzung und ermöglicht das Setzen von Schreibgrenzen" + } + }, + "advanced": { + "data": { + "ac_voltage": "Die Wechselspannung Ihres Netzes in V", + "ac_current": "Die Wechselstromgrenze (pro Phase) Ihres Netzes in A", + "dc_voltage": "Die Gleichspannung Ihrer Batterie in V", + "dc_current": "Die Gleichstromgrenze Ihrer Batterie in A", + "number_of_phases": "Die Phasenkonfiguration Ihres Systems", + "use_sliders": "Verwendet abgestufte Schieberegler fĂŒr beschreibbare Zahlenwerte" + } + }, + "reconfigure": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "VerfĂŒgbare GerĂ€te erneut scannen. Dadurch werden alle verfĂŒgbaren GerĂ€te erneut gescannt", + "interval": "Aktualisierungsintervall in (s)", + "ac_voltage": "Die Wechselspannung Ihres Netzes in V", + "ac_current": "Die Wechselstromgrenze (pro Phase) Ihres Netzes in A", + "dc_voltage": "Die Gleichspannung Ihrer Batterie in V", + "dc_current": "Die Gleichstromgrenze Ihrer Batterie in A", + "number_of_phases": "Die Phasenkonfiguration Ihres Systems", + "use_sliders": "Verwendet abgestufte Schieberegler fĂŒr beschreibbare Zahlenwerte", + "advanced": "Wechselt in den Nur-Lesen-Modus, wenn nicht markiert (beim Absenden)" + } + }, + "init_read": { + "data": { + "rescan": "VerfĂŒgbare GerĂ€te erneut scannen. Dadurch werden alle verfĂŒgbaren GerĂ€te erneut gescannt", + "interval": "Aktualisierungsintervall in (s)", + "advanced": "SchreibunterstĂŒtzung aktivieren" + } + }, + "advanced": { + "data": { + "ac_voltage": "Die Wechselspannung Ihres Netzes in V", + "ac_current": "Die Wechselstromgrenze Ihres Netzes in A", + "dc_voltage": "Die Gleichspannung Ihrer Batterie in V", + "dc_current": "Die Gleichstromgrenze Ihrer Batterie in A", + "number_of_phases": "Die Phasenkonfiguration Ihres Systems", + "use_sliders": "Verwendet abgestufte Schieberegler fĂŒr beschreibbare Zahlenwerte" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/victron/translations/es.json b/custom_components/victron/translations/es.json new file mode 100644 index 0000000..4f7ddb2 --- /dev/null +++ b/custom_components/victron/translations/es.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya estĂĄ configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "AutenticaciĂłn invĂĄlida", + "unknown": "Error inesperado", + "already_configured": "Solo se admite una instancia de la integraciĂłn Victron en este momento" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto", + "interval": "Intervalo de actualizaciĂłn en (s)", + "advanced": "Habilita el soporte de escritura y permite establecer lĂ­mites de escritura" + } + }, + "advanced": { + "data": { + "ac_voltage": "El voltaje AC de tu red en V", + "ac_current": "El lĂ­mite de corriente AC (por fase) de tu red en A", + "dc_voltage": "El voltaje DC de tu baterĂ­a en V", + "dc_current": "El lĂ­mite de corriente DC de tu baterĂ­a en A", + "number_of_phases": "La configuraciĂłn de fases de tu sistema", + "use_sliders": "Usar controles deslizantes escalonados para entidades numĂ©ricas editables" + } + }, + "reconfigure": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "Reescanear dispositivos disponibles. Esto reescanearĂĄ todos los dispositivos disponibles", + "interval": "Intervalo de actualizaciĂłn en (s)", + "ac_voltage": "El voltaje AC de tu red en V", + "ac_current": "El lĂ­mite de corriente AC (por fase) de tu red en A", + "dc_voltage": "El voltaje DC de tu baterĂ­a en V", + "dc_current": "El lĂ­mite de corriente DC de tu baterĂ­a en A", + "number_of_phases": "La configuraciĂłn de fases de tu sistema", + "use_sliders": "Usar controles deslizantes escalonados para entidades numĂ©ricas editables", + "advanced": "Cambiar al modo solo lectura si no estĂĄ seleccionado (al enviar)" + } + }, + "init_read": { + "data": { + "rescan": "Reescanear dispositivos disponibles. Esto reescanearĂĄ todos los dispositivos disponibles", + "interval": "Intervalo de actualizaciĂłn en (s)", + "advanced": "Habilitar soporte de escritura" + } + }, + "advanced": { + "data": { + "ac_voltage": "El voltaje AC de tu red en V", + "ac_current": "El lĂ­mite de corriente AC de tu red en A", + "dc_voltage": "El voltaje DC de tu baterĂ­a en V", + "dc_current": "El lĂ­mite de corriente DC de tu baterĂ­a en A", + "number_of_phases": "La configuraciĂłn de fases de tu sistema", + "use_sliders": "Usar controles deslizantes escalonados para entidades numĂ©ricas editables" + } + } + } + } +} diff --git a/custom_components/victron/translations/fr.json b/custom_components/victron/translations/fr.json new file mode 100644 index 0000000..5abe455 --- /dev/null +++ b/custom_components/victron/translations/fr.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est dĂ©jĂ  configurĂ©" + }, + "error": { + "cannot_connect": "Échec de la connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue", + "already_configured": "Une seule instance de l'intĂ©gration Victron est prise en charge pour le moment" + }, + "step": { + "user": { + "data": { + "host": "HĂŽte", + "port": "Port", + "interval": "Intervalle de mise Ă  jour en (s)", + "advanced": "Active le support en Ă©criture et permet de dĂ©finir des limites d'Ă©criture" + } + }, + "advanced": { + "data": { + "ac_voltage": "La tension AC de votre rĂ©seau en V", + "ac_current": "La limite de courant AC (par phase) de votre rĂ©seau en A", + "dc_voltage": "La tension DC de votre batterie en V", + "dc_current": "La limite de courant DC de votre batterie en A", + "number_of_phases": "La configuration de phase de votre systĂšme", + "use_sliders": "Utilise des curseurs graduĂ©s pour les entitĂ©s numĂ©riques modifiables" + } + }, + "reconfigure": { + "data": { + "host": "HĂŽte", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "Rescanner les appareils disponibles. Cela rescannera tous les appareils disponibles", + "interval": "Intervalle de mise Ă  jour en (s)", + "ac_voltage": "La tension AC de votre rĂ©seau en V", + "ac_current": "La limite de courant AC (par phase) de votre rĂ©seau en A", + "dc_voltage": "La tension DC de votre batterie en V", + "dc_current": "La limite de courant DC de votre batterie en A", + "number_of_phases": "La configuration de phase de votre systĂšme", + "use_sliders": "Utilise des curseurs graduĂ©s pour les entitĂ©s numĂ©riques modifiables", + "advanced": "Basculer en mode lecture seule si dĂ©cochĂ© (lors de l'envoi)" + } + }, + "init_read": { + "data": { + "rescan": "Rescanner les appareils disponibles. Cela rescannera tous les appareils disponibles", + "interval": "Intervalle de mise Ă  jour en (s)", + "advanced": "Activer le support en Ă©criture" + } + }, + "advanced": { + "data": { + "ac_voltage": "La tension AC de votre rĂ©seau en V", + "ac_current": "La limite de courant AC de votre rĂ©seau en A", + "dc_voltage": "La tension DC de votre batterie en V", + "dc_current": "La limite de courant DC de votre batterie en A", + "number_of_phases": "La configuration de phase de votre systĂšme", + "use_sliders": "Utilise des curseurs graduĂ©s pour les entitĂ©s numĂ©riques modifiables" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/victron/translations/it.json b/custom_components/victron/translations/it.json new file mode 100644 index 0000000..05b7a9c --- /dev/null +++ b/custom_components/victron/translations/it.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo Ăš giĂ  configurato" + }, + "error": { + "cannot_connect": "Connessione fallita", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore inaspettato", + "already_configured": "È supportata solo un'istanza dell'integrazione Victron al momento" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta", + "interval": "Intervallo di aggiornamento in (s)", + "advanced": "Abilita il supporto in scrittura e consente di impostare limiti di scrittura" + } + }, + "advanced": { + "data": { + "ac_voltage": "La tensione AC della tua rete in V", + "ac_current": "Il limite di corrente AC (per fase) della tua rete in A", + "dc_voltage": "La tensione DC della tua batteria in V", + "dc_current": "Il limite di corrente DC della tua batteria in A", + "number_of_phases": "La configurazione delle fasi del tuo sistema", + "use_sliders": "Utilizza cursori a passi per entitĂ  numeriche scrivibili" + } + }, + "reconfigure": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "Esegui nuovamente la scansione dei dispositivi disponibili. Questo eseguirĂ  la scansione di tutti i dispositivi disponibili", + "interval": "Intervallo di aggiornamento in (s)", + "ac_voltage": "La tensione AC della tua rete in V", + "ac_current": "Il limite di corrente AC (per fase) della tua rete in A", + "dc_voltage": "La tensione DC della tua batteria in V", + "dc_current": "Il limite di corrente DC della tua batteria in A", + "number_of_phases": "La configurazione delle fasi del tuo sistema", + "use_sliders": "Utilizza cursori a passi per entitĂ  numeriche scrivibili", + "advanced": "Passa alla modalitĂ  sola lettura se deselezionato (al momento dell'invio)" + } + }, + "init_read": { + "data": { + "rescan": "Esegui nuovamente la scansione dei dispositivi disponibili. Questo eseguirĂ  la scansione di tutti i dispositivi disponibili", + "interval": "Intervallo di aggiornamento in (s)", + "advanced": "Abilita il supporto in scrittura" + } + }, + "advanced": { + "data": { + "ac_voltage": "La tensione AC della tua rete in V", + "ac_current": "Il limite di corrente AC della tua rete in A", + "dc_voltage": "La tensione DC della tua batteria in V", + "dc_current": "Il limite di corrente DC della tua batteria in A", + "number_of_phases": "La configurazione delle fasi del tuo sistema", + "use_sliders": "Utilizza cursori a passi per entitĂ  numeriche scrivibili" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/victron/translations/nl.json b/custom_components/victron/translations/nl.json new file mode 100644 index 0000000..7a8c549 --- /dev/null +++ b/custom_components/victron/translations/nl.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout", + "already_configured": "Er wordt op dit moment slechts Ă©Ă©n instance van de Victron-integratie ondersteund" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort", + "interval": "Update-interval in (s)", + "advanced": "Schakelt schrijfondersteuning in en maakt het instellen van schrijfbeperkingen mogelijk" + } + }, + "advanced": { + "data": { + "ac_voltage": "De AC-spanning van uw netwerk in V", + "ac_current": "De AC (per fase) stroomlimiet van uw netwerk in A", + "dc_voltage": "De DC-spanning van uw batterij in V", + "dc_current": "De DC-stroomlimiet van uw batterij in A", + "number_of_phases": "De faseconfiguratie van uw systeem", + "use_sliders": "Gebruik trapsgewijze schuifregelaars voor beschrijfbare numerieke eenheden" + } + }, + "reconfigure": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "Beschikbare apparaten opnieuw scannen. Dit scant alle beschikbare apparaten opnieuw", + "interval": "Update-interval in (s)", + "ac_voltage": "De AC-spanning van uw netwerk in V", + "ac_current": "De AC (per fase) stroomlimiet van uw netwerk in A", + "dc_voltage": "De DC-spanning van uw batterij in V", + "dc_current": "De DC-stroomlimiet van uw batterij in A", + "number_of_phases": "De faseconfiguratie van uw systeem", + "use_sliders": "Gebruik trapsgewijze schuifregelaars voor beschrijfbare numerieke eenheden", + "advanced": "Schakel over naar alleen-lezen modus als uitgeschakeld (bij indienen)" + } + }, + "init_read": { + "data": { + "rescan": "Beschikbare apparaten opnieuw scannen. Dit scant alle beschikbare apparaten opnieuw", + "interval": "Update-interval in (s)", + "advanced": "Schrijfondersteuning inschakelen" + } + }, + "advanced": { + "data": { + "ac_voltage": "De AC-spanning van uw netwerk in V", + "ac_current": "De AC-stroomlimiet van uw netwerk in A", + "dc_voltage": "De DC-spanning van uw batterij in V", + "dc_current": "De DC-stroomlimiet van uw batterij in A", + "number_of_phases": "De faseconfiguratie van uw systeem", + "use_sliders": "Gebruik trapsgewijze schuifregelaars voor beschrijfbare numerieke eenheden" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/victron/translations/sv.json b/custom_components/victron/translations/sv.json new file mode 100644 index 0000000..9254244 --- /dev/null +++ b/custom_components/victron/translations/sv.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten Ă€r redan konfigurerad" + }, + "error": { + "cannot_connect": "Misslyckades med att ansluta", + "invalid_auth": "Ogiltig autentisering", + "unknown": "OvĂ€ntat fel", + "already_configured": "Endast en instans av Victron-integrationen stöds för nĂ€rvarande" + }, + "step": { + "user": { + "data": { + "host": "VĂ€rd", + "port": "Port", + "interval": "Uppdateringsintervall i (s)", + "advanced": "Aktiverar skrivstöd och möjliggör instĂ€llning av skrivgrĂ€nser" + } + }, + "advanced": { + "data": { + "ac_voltage": "VĂ€xelspĂ€nningen i ditt elnĂ€t i V", + "ac_current": "VĂ€xelströmbegrĂ€nsningen (per fas) i ditt elnĂ€t i A", + "dc_voltage": "LikspĂ€nningen i ditt batteri i V", + "dc_current": "LikströmsbegrĂ€nsningen i ditt batteri i A", + "number_of_phases": "Faskonfigurationen i ditt system", + "use_sliders": "AnvĂ€nder stegade reglage för skrivbara numeriska vĂ€rden" + } + }, + "reconfigure": { + "data": { + "host": "VĂ€rd", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init_write": { + "data": { + "rescan": "Skanna om tillgĂ€ngliga enheter. Detta kommer att skanna om alla tillgĂ€ngliga enheter", + "interval": "Uppdateringsintervall i (s)", + "ac_voltage": "VĂ€xelspĂ€nningen i ditt elnĂ€t i V", + "ac_current": "VĂ€xelströmbegrĂ€nsningen (per fas) i ditt elnĂ€t i A", + "dc_voltage": "LikspĂ€nningen i ditt batteri i V", + "dc_current": "LikströmsbegrĂ€nsningen i ditt batteri i A", + "number_of_phases": "Faskonfigurationen i ditt system", + "use_sliders": "AnvĂ€nder stegade reglage för skrivbara numeriska vĂ€rden", + "advanced": "VĂ€xla till skrivskyddat lĂ€ge om det inte Ă€r markerat (vid insĂ€ndning)" + } + }, + "init_read": { + "data": { + "rescan": "Skanna om tillgĂ€ngliga enheter. Detta kommer att skanna om alla tillgĂ€ngliga enheter", + "interval": "Uppdateringsintervall i (s)", + "advanced": "Aktivera skrivstöd" + } + }, + "advanced": { + "data": { + "ac_voltage": "VĂ€xelspĂ€nningen i ditt elnĂ€t i V", + "ac_current": "VĂ€xelströmbegrĂ€nsningen i ditt elnĂ€t i A", + "dc_voltage": "LikspĂ€nningen i ditt batteri i V", + "dc_current": "LikströmsbegrĂ€nsningen i ditt batteri i A", + "number_of_phases": "Faskonfigurationen i ditt system", + "use_sliders": "AnvĂ€nder stegade reglage för skrivbara numeriska vĂ€rden" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json index e8053fd..65d07db 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,9 @@ { "name": "Victron GX modbus TCP", "render_readme": true, - "homeassistant": "2023.9.1", + "zip_release": true, + "filename": "victron.zip", + "hide_default_branch": false, + "homeassistant": "2025.1", "hacs": "1.28.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f77e935 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,613 @@ +[build-system] +requires = ["setuptools==75.1.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "hass-victron" +version = "0.5.0" +license = {text = "Apache-2.0"} +description = "Open-source integration for the victron modbus endpoint" +readme = "README.md" +keywords = ["home", "automation","homeassistant","victron"] +requires-python = ">=3.13.0" + +[project.urls] +"Source Code" = "https://github.com/sfstar/hass-victron" +"Bug Reports" = "https://github.com/home-assistant/sfstar/hass-victron/issues" + +[tool.pylint.MAIN] +py-version = "3.13" +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs = 2 +init-hook = """\ + from pathlib import Path; \ + import sys; \ + + from pylint.config import find_default_config_files; \ + + sys.path.append( \ + str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) + ) \ + """ +load-plugins = [ + "pylint.extensions.code_style", + "pylint.extensions.typing", + "pylint_per_file_ignores", +] +persistent = false +extension-pkg-allow-list = [ + "av.audio.stream", + "av.logging", + "av.stream", + "ciso8601", + "orjson", + "cv2", +] +fail-on = [ + "I", +] + +[tool.pylint.BASIC] +class-const-naming-style = "any" + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by ruff +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +# possibly-used-before-assignment - too many errors / not necessarily issues +# --- +# Pylint CodeStyle plugin +# consider-using-namedtuple-or-dataclass - too opinionated +# consider-using-assignment-expr - decision to use := better left to devs +disable = [ + "format", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-boolean-expressions", + "too-many-positional-arguments", + "wrong-import-order", + "consider-using-namedtuple-or-dataclass", + "consider-using-assignment-expr", + "possibly-used-before-assignment", + + # Handled by ruff + # Ref: + "await-outside-async", # PLE1142 + "bad-str-strip-call", # PLE1310 + "bad-string-format-type", # PLE1307 + "bidirectional-unicode", # PLE2502 + "continue-in-finally", # PLE0116 + "duplicate-bases", # PLE0241 + "misplaced-bare-raise", # PLE0704 + "format-needs-mapping", # F502 + "function-redefined", # F811 + # Needed because ruff does not understand type of __all__ generated by a function + # "invalid-all-format", # PLE0605 + "invalid-all-object", # PLE0604 + "invalid-character-backspace", # PLE2510 + "invalid-character-esc", # PLE2513 + "invalid-character-nul", # PLE2514 + "invalid-character-sub", # PLE2512 + "invalid-character-zero-width-space", # PLE2515 + "logging-too-few-args", # PLE1206 + "logging-too-many-args", # PLE1205 + "missing-format-string-key", # F524 + "mixed-format-string", # F506 + "no-method-argument", # N805 + "no-self-argument", # N805 + "nonexistent-operator", # B002 + "nonlocal-without-binding", # PLE0117 + "not-in-loop", # F701, F702 + "notimplemented-raised", # F901 + "return-in-init", # PLE0101 + "return-outside-function", # F706 + "syntax-error", # E999 + "too-few-format-args", # F524 + "too-many-format-args", # F522 + "too-many-star-expressions", # F622 + "truncated-format-string", # F501 + "undefined-all-variable", # F822 + "undefined-variable", # F821 + "used-prior-global-declaration", # PLE0118 + "yield-inside-async-function", # PLE1700 + "yield-outside-function", # F704 + "anomalous-backslash-in-string", # W605 + "assert-on-string-literal", # PLW0129 + "assert-on-tuple", # F631 + "bad-format-string", # W1302, F + "bad-format-string-key", # W1300, F + "bare-except", # E722 + "binary-op-exception", # PLW0711 + "cell-var-from-loop", # B023 + # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work + "duplicate-except", # B014 + "duplicate-key", # F601 + "duplicate-string-formatting-argument", # F + "duplicate-value", # F + "eval-used", # S307 + "exec-used", # S102 + "expression-not-assigned", # B018 + "f-string-without-interpolation", # F541 + "forgotten-debug-statement", # T100 + "format-string-without-interpolation", # F + # "global-statement", # PLW0603, ruff catches new occurrences, needs more work + "global-variable-not-assigned", # PLW0602 + "implicit-str-concat", # ISC001 + "import-self", # PLW0406 + "inconsistent-quotes", # Q000 + "invalid-envvar-default", # PLW1508 + "keyword-arg-before-vararg", # B026 + "logging-format-interpolation", # G + "logging-fstring-interpolation", # G + "logging-not-lazy", # G + "misplaced-future", # F404 + "named-expr-without-context", # PLW0131 + "nested-min-max", # PLW3301 + "pointless-statement", # B018 + "raise-missing-from", # B904 + "redefined-builtin", # A001 + "try-except-raise", # TRY302 + "unused-argument", # ARG001, we don't use it + "unused-format-string-argument", #F507 + "unused-format-string-key", # F504 + "unused-import", # F401 + "unused-variable", # F841 + "useless-else-on-loop", # PLW0120 + "wildcard-import", # F403 + "bad-classmethod-argument", # N804 + "consider-iterating-dictionary", # SIM118 + "empty-docstring", # D419 + "invalid-name", # N815 + "line-too-long", # E501, disabled globally + "missing-class-docstring", # D101 + "missing-final-newline", # W292 + "missing-function-docstring", # D103 + "missing-module-docstring", # D100 + "multiple-imports", #E401 + "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 + "superfluous-parens", # UP034 + "ungrouped-imports", # I001 + "unidiomatic-typecheck", # E721 + "unnecessary-direct-lambda-call", # PLC3002 + "unnecessary-lambda-assignment", # PLC3001 + "unnecessary-pass", # PIE790 + "unneeded-not", # SIM208 + "useless-import-alias", # PLC0414 + "wrong-import-order", # I001 + "wrong-import-position", # E402 + "comparison-of-constants", # PLR0133 + "comparison-with-itself", # PLR0124 + "consider-alternative-union-syntax", # UP007 + "consider-merging-isinstance", # PLR1701 + "consider-using-alias", # UP006 + "consider-using-dict-comprehension", # C402 + "consider-using-generator", # C417 + "consider-using-get", # SIM401 + "consider-using-set-comprehension", # C401 + "consider-using-sys-exit", # PLR1722 + "consider-using-ternary", # SIM108 + "literal-comparison", # F632 + "property-with-parameters", # PLR0206 + "super-with-arguments", # UP008 + "too-many-branches", # PLR0912 + "too-many-return-statements", # PLR0911 + "too-many-statements", # PLR0915 + "trailing-comma-tuple", # COM818 + "unnecessary-comprehension", # C416 + "use-a-generator", # C417 + "use-dict-literal", # C406 + "use-list-literal", # C405 + "useless-object-inheritance", # UP004 + "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 + "broad-except", # BLE001 + "protected-access", # SLF001 + "broad-exception-raised", # TRY002 + "consider-using-f-string", # PLC0209 + # "no-self-use", # PLR6301 # Optional plugin, not enabled + + # Handled by mypy + # Ref: + "abstract-class-instantiated", + "arguments-differ", + "assigning-non-slot", + "assignment-from-no-return", + "assignment-from-none", + "bad-exception-cause", + "bad-format-character", + "bad-reversed-sequence", + "bad-super-call", + "bad-thread-instantiation", + "catching-non-exception", + "comparison-with-callable", + "deprecated-class", + "dict-iter-missing-items", + "format-combined-specification", + "global-variable-undefined", + "import-error", + "inconsistent-mro", + "inherit-non-class", + "init-is-generator", + "invalid-class-object", + "invalid-enum-extension", + "invalid-envvar-value", + "invalid-format-returned", + "invalid-hash-returned", + "invalid-metaclass", + "invalid-overridden-method", + "invalid-repr-returned", + "invalid-sequence-index", + "invalid-slice-index", + "invalid-slots-object", + "invalid-slots", + "invalid-star-assignment-target", + "invalid-str-returned", + "invalid-unary-operand-type", + "invalid-unicode-codec", + "isinstance-second-argument-not-valid-type", + "method-hidden", + "misplaced-format-function", + "missing-format-argument-key", + "missing-format-attribute", + "missing-kwoa", + "no-member", + "no-value-for-parameter", + "non-iterator-returned", + "non-str-assignment-to-dunder-name", + "nonlocal-and-global", + "not-a-mapping", + "not-an-iterable", + "not-async-context-manager", + "not-callable", + "not-context-manager", + "overridden-final-method", + "raising-bad-type", + "raising-non-exception", + "redundant-keyword-arg", + "relative-beyond-top-level", + "self-cls-assignment", + "signature-differs", + "star-needs-assignment-target", + "subclassed-final-class", + "super-without-brackets", + "too-many-function-args", + "typevar-double-variance", + "typevar-name-mismatch", + "unbalanced-dict-unpacking", + "unbalanced-tuple-unpacking", + "unexpected-keyword-arg", + "unhashable-member", + "unpacking-non-sequence", + "unsubscriptable-object", + "unsupported-assignment-operation", + "unsupported-binary-operation", + "unsupported-delete-operation", + "unsupported-membership-test", + "used-before-assignment", + "using-final-decorator-in-unsupported-version", + "wrong-exception-operation", +] +enable = [ + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] +per-file-ignores = [ + # redefined-outer-name: Tests reference fixtures in the test function + # use-implicit-booleaness-not-comparison: Tests need to validate that a list + # or a dict is returned + "/tests/:redefined-outer-name,use-implicit-booleaness-not-comparison", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.TYPECHECK] +ignored-classes = [ + "_CountingAttr", # for attrs +] +mixin-class-rgx = ".*[Mm]ix[Ii]n" + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.EXCEPTIONS] +overgeneral-exceptions = [ + "builtins.BaseException", + "builtins.Exception", + # "homeassistant.exceptions.HomeAssistantError", # too many issues +] + +[tool.pylint.TYPING] +runtime-typing = false + +[tool.pylint.CODE_STYLE] +max-line-length-suggestions = 72 + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +norecursedirs = [ + ".git", + "testing_config", +] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" + +[tool.ruff] +required-version = ">=0.9.1" + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC210", # Async functions should not call blocking HTTP methods + "ASYNC220", # Async functions should not create subprocesses with blocking methods + "ASYNC221", # Async functions should not run processes with blocking methods + "ASYNC222", # Async functions should not wait on processes with blocking methods + "ASYNC230", # Async functions should not open files with blocking methods like open + "ASYNC251", # Async functions should not call time.sleep + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil + "B018", # Found useless attribute access. Either assign it to a variable or remove it. + "B023", # Function definition does not bind loop variable {name} + "B024", # `{name}` is an abstract base class, but it has no abstract methods or properties + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B035", # Dictionary comprehension uses static key + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs + "RUF008", # Do not use mutable default values for dataclass attributes + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer + "RUF017", # Avoid quadratic list summation + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + "RUF020", # {never_like} | T is equivalent to T + "RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear + "RUF022", # Sort __all__ + "RUF023", # Sort __slots__ + "RUF024", # Do not pass mutable objects as values to dict.fromkeys + "RUF026", # default_factory is a positional-only argument to defaultdict + "RUF030", # print() call in assert statement is likely unintentional + "RUF032", # Decimal() called with float literal argument + "RUF033", # __post_init__ method with argument defaults + "RUF034", # Useless if-else condition + "RUF100", # Unused `noqa` directive + "RUF101", # noqa directives that use redirected rule codes + "RUF200", # Failed to parse pyproject.toml: {message} + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TC", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block + + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` + # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605" +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +voluptuous = "vol" +"homeassistant.components.air_quality.PLATFORM_SCHEMA" = "AIR_QUALITY_PLATFORM_SCHEMA" +"homeassistant.components.alarm_control_panel.PLATFORM_SCHEMA" = "ALARM_CONTROL_PANEL_PLATFORM_SCHEMA" +"homeassistant.components.binary_sensor.PLATFORM_SCHEMA" = "BINARY_SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.button.PLATFORM_SCHEMA" = "BUTTON_PLATFORM_SCHEMA" +"homeassistant.components.calendar.PLATFORM_SCHEMA" = "CALENDAR_PLATFORM_SCHEMA" +"homeassistant.components.camera.PLATFORM_SCHEMA" = "CAMERA_PLATFORM_SCHEMA" +"homeassistant.components.climate.PLATFORM_SCHEMA" = "CLIMATE_PLATFORM_SCHEMA" +"homeassistant.components.conversation.PLATFORM_SCHEMA" = "CONVERSATION_PLATFORM_SCHEMA" +"homeassistant.components.cover.PLATFORM_SCHEMA" = "COVER_PLATFORM_SCHEMA" +"homeassistant.components.date.PLATFORM_SCHEMA" = "DATE_PLATFORM_SCHEMA" +"homeassistant.components.datetime.PLATFORM_SCHEMA" = "DATETIME_PLATFORM_SCHEMA" +"homeassistant.components.device_tracker.PLATFORM_SCHEMA" = "DEVICE_TRACKER_PLATFORM_SCHEMA" +"homeassistant.components.event.PLATFORM_SCHEMA" = "EVENT_PLATFORM_SCHEMA" +"homeassistant.components.fan.PLATFORM_SCHEMA" = "FAN_PLATFORM_SCHEMA" +"homeassistant.components.geo_location.PLATFORM_SCHEMA" = "GEO_LOCATION_PLATFORM_SCHEMA" +"homeassistant.components.humidifier.PLATFORM_SCHEMA" = "HUMIDIFIER_PLATFORM_SCHEMA" +"homeassistant.components.image.PLATFORM_SCHEMA" = "IMAGE_PLATFORM_SCHEMA" +"homeassistant.components.image_processing.PLATFORM_SCHEMA" = "IMAGE_PROCESSING_PLATFORM_SCHEMA" +"homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" +"homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" +"homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" +"homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" +"homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" +"homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" +"homeassistant.components.remote.PLATFORM_SCHEMA" = "REMOTE_PLATFORM_SCHEMA" +"homeassistant.components.scene.PLATFORM_SCHEMA" = "SCENE_PLATFORM_SCHEMA" +"homeassistant.components.select.PLATFORM_SCHEMA" = "SELECT_PLATFORM_SCHEMA" +"homeassistant.components.sensor.PLATFORM_SCHEMA" = "SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.siren.PLATFORM_SCHEMA" = "SIREN_PLATFORM_SCHEMA" +"homeassistant.components.stt.PLATFORM_SCHEMA" = "STT_PLATFORM_SCHEMA" +"homeassistant.components.switch.PLATFORM_SCHEMA" = "SWITCH_PLATFORM_SCHEMA" +"homeassistant.components.text.PLATFORM_SCHEMA" = "TEXT_PLATFORM_SCHEMA" +"homeassistant.components.time.PLATFORM_SCHEMA" = "TIME_PLATFORM_SCHEMA" +"homeassistant.components.todo.PLATFORM_SCHEMA" = "TODO_PLATFORM_SCHEMA" +"homeassistant.components.tts.PLATFORM_SCHEMA" = "TTS_PLATFORM_SCHEMA" +"homeassistant.components.vacuum.PLATFORM_SCHEMA" = "VACUUM_PLATFORM_SCHEMA" +"homeassistant.components.valve.PLATFORM_SCHEMA" = "VALVE_PLATFORM_SCHEMA" +"homeassistant.components.update.PLATFORM_SCHEMA" = "UPDATE_PLATFORM_SCHEMA" +"homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" +"homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" +"homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" +"homeassistant.core.DOMAIN" = "HOMEASSISTANT_DOMAIN" +"homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.category_registry" = "cr" +"homeassistant.helpers.config_validation" = "cv" +"homeassistant.helpers.device_registry" = "dr" +"homeassistant.helpers.entity_registry" = "er" +"homeassistant.helpers.floor_registry" = "fr" +"homeassistant.helpers.issue_registry" = "ir" +"homeassistant.helpers.label_registry" = "lr" +"homeassistant.util.color" = "color_util" +"homeassistant.util.dt" = "dt_util" +"homeassistant.util.json" = "json_util" +"homeassistant.util.location" = "location_util" +"homeassistant.util.logging" = "logging_util" +"homeassistant.util.network" = "network_util" +"homeassistant.util.ulid" = "ulid_util" +"homeassistant.util.uuid" = "uuid_util" +"homeassistant.util.yaml" = "yaml_util" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" +"pytz".msg = "use zoneinfo instead" +"tests".msg = "You should not import tests" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [ + "homeassistant", +] +combine-as-imports = true +split-on-trailing-comma = false + +#[tool.ruff.lint.per-file-ignores] + + + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +property-decorators = ["propcache.api.cached_property"] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..a137396 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,12 @@ +# This extend our general Ruff rules specifically for tests +extend = "pyproject.toml" + +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] + +[lint.isort] +known-third-party = [ + "pylint", +]