diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c4b17d7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_flake diff --git a/.github/workflows/poetry-build-test-release.yml b/.github/workflows/poetry-build-test-release.yml new file mode 100644 index 0000000..4989e00 --- /dev/null +++ b/.github/workflows/poetry-build-test-release.yml @@ -0,0 +1,48 @@ +name: Build, Test Release + +on: + push: + branches: [ "main", "master" ] + tags: + - "v*" + pull_request: + branches: [ "main", "master" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Install dependencies + run: | + uv lock + - name: Build + run: | + uv build + - name: Test + run: | + uv run pytest + - name: Test Nix build + run: | + nix build . + - name: Lint with flake8 + continue-on-error: true + run: | + # stop the build if there are Python syntax errors or undefined names + uv run flake8 ./src --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + uv run flake8 ./src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Publish distribution 📦 to PyPI + if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' + run: | + uv publish --token ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml deleted file mode 100644 index 73a722c..0000000 --- a/.github/workflows/pypi-release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: PyPi Release - -on: - push: - tags: - - 'v*' -# based on https://github.com/pypa/gh-action-pypi-publish - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - - name: Install dependencies - run: >- - python -m pip install --user --upgrade setuptools wheel - - name: Build - run: >- - python setup.py sdist bdist_wheel - - name: Publish distribution 📦 to PyPI - if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml deleted file mode 100644 index f4bcc8a..0000000 --- a/.github/workflows/python-app.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python application - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest serial construct - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..909c14f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.direnv +dist + +__pycache__ +*.egg-info +.idea + +result \ No newline at end of file diff --git a/README.md b/README.md index 135417f..45f4663 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,29 @@ Container: This lib depends on `pyserial` and the awesome `construct` lib. + +## How to run demos + +TCP demo: + +```bash +uv run python ./demos/test-tcp.py 192.168.1.7 10 +``` + +Serial: + +```bash +socat -v pty,link=/tmp/serial,waitslave tcp:192.168.1.7:23,forever +# in another terminal +uv run python ./demos/test-serial.py /tmp/serial 1 +``` + +## How to run mongodb collector + +```bash +uv run poller 192.168.1.7 --mongo-url mongodb://mongodb.local:27017 --interval 1000 +``` + # Hardware wiring The pylontech modules talk using the RS485 line protocol. ## Pylontech side @@ -102,4 +125,5 @@ If you are using US2000 and US3000 batteries, then the main battery must be a US ## Using Pylontech LV Hub with multible battery banks -If the LV hub is used the address of the RS485 devices is depending on the battery bank. To read values the specific device address is needed. To scan for devices on a bank you can use the `scan_for_batteries` function. The max range is 0 to 255. \ No newline at end of file +If the LV hub is used the address of the RS485 devices is depending on the battery bank. To read values the specific device address is needed. To scan for devices on a bank you can use the `scan_for_batteries` function. The max range is 0 to 255. + diff --git a/demos/test-serial.py b/demos/test-serial.py new file mode 100644 index 0000000..d06f867 --- /dev/null +++ b/demos/test-serial.py @@ -0,0 +1,44 @@ +from time import sleep + +from pylontech import * + +if __name__ == '__main__': + iters = 0 + + import sys + from rich import print_json + import json + + # socat -v pty,link=/tmp/serial,waitslave tcp:192.168.10.237:23,forever + if len(sys.argv) < 2: + print("Usage: python test-tcp.py ") + exit(1) + + host = sys.argv[1] + iterations = sys.argv[2] + + cont = lambda iter: iter < 1 + if iterations == "inf": + cont = lambda iter: True + if iterations != "inf": + cont = lambda iter: iter < int(iterations) + + p = Pylontech(SerialDeviceTransport(serial_port=host, baudrate=115200)) + bats = p.scan_for_batteries(2, 10) + print("Battery stack:") + print_json(json.dumps(to_json_serializable(bats))) + + cc = 0 + + try: + for b in p.poll_parameters(bats.range()): + cc += 1 + if not cont(cc): + break + print("System state:") + print_json(json.dumps(b)) + sleep(0.5) + except (KeyboardInterrupt, SystemExit): + exit(0) + except BaseException as e: + raise e diff --git a/demos/test-tcp.py b/demos/test-tcp.py new file mode 100644 index 0000000..c0675c5 --- /dev/null +++ b/demos/test-tcp.py @@ -0,0 +1,48 @@ +from time import sleep + +from pylontech import * + + +if __name__ == '__main__': + """ + Direct TCP connections to devices like Waveshare RS485 to ETH, are 20-50 times faster than + serial port emulation through socat. Turn "RFC2217" option on. + """ + iters = 0 + + import sys + from rich import print_json + import json + + if len(sys.argv) < 2: + print("Usage: python test-tcp.py ") + exit(1) + + host = sys.argv[1] + iterations = sys.argv[2] + + cont = lambda iter: iter < 1 + if iterations == "inf": + cont = lambda iter: True + if iterations != "inf": + cont = lambda iter: iter < int(iterations) + + p = Pylontech(ExscriptTelnetTransport(host=host, port=23)) + bats = p.scan_for_batteries(2, 10) + print("Battery stack:") + print_json(json.dumps(to_json_serializable(bats))) + + cc = 0 + + try: + for b in p.poll_parameters(bats.range()): + cc += 1 + if not cont(cc): + break + print("System state:") + print_json(json.dumps(b)) + sleep(0.5) + except (KeyboardInterrupt, SystemExit): + exit(0) + except BaseException as e: + raise e diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fca86ea --- /dev/null +++ b/flake.lock @@ -0,0 +1,133 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1748624050, + "narHash": "sha256-wvVeiiM2jyxq5lylycigpsFUCWQ/jqgabyAogv04How=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5f161237cbfdfea721e5d69c13075327c7c8054c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ], + "uv2nix": [ + "uv2nix" + ] + }, + "locked": { + "lastModified": 1748562898, + "narHash": "sha256-STk4QklrGpM3gliPKNJdBLSQvIrqRuwHI/rnYb/5rh8=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "33bd58351957bb52dd1700ea7eeefe34de06a892", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746540146, + "narHash": "sha256-QxdHGNpbicIrw5t6U3x+ZxeY/7IEJ6lYbvsjXmcxFIM=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "e09c10c24ebb955125fda449939bfba664c467fd", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1748398512, + "narHash": "sha256-99mf47Kjl/rj716cSjeA6ubZLlhNudmC4HRg/6UMfvs=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "f006d191d4ff5894d2ead6299e2eaf3659bc46b0", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..11f3662 --- /dev/null +++ b/flake.nix @@ -0,0 +1,91 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/release-25.05"; + + inputs.flake-utils.url = "github:numtide/flake-utils"; + + inputs.pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + inputs.uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + inputs.pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # ( printf "~20024642E00202FD33\r"; sleep 1 ) | nc 192.168.10.237 23 + outputs = + { self + , nixpkgs + , flake-utils + , uv2nix + , pyproject-nix + , pyproject-build-systems + , ... + }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + lib = pkgs.lib; + workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; + + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; # or sourcePreference = "sdist"; + }; + + pyprojectOverrides = _final: _prev: { + }; + + python = pkgs.python313; + + pythonSet = + (pkgs.callPackage pyproject-nix.build.packages { + inherit python; + }).overrideScope + ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + pyprojectOverrides + ] + ); + in + { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + git + socat + uv + + (python313.withPackages (python-pkgs: [ + python-pkgs.pyserial + python-pkgs.construct + python-pkgs.standard-telnetlib + python-pkgs.rich + ])) + ]; + }; + + packages.default = pythonSet.mkVirtualEnv "pylontechpoller-env" workspace.deps.default; + + apps.default = { + type = "app"; + program = "${self.packages."${system}".default}/bin/poller"; + }; + } + ); + + +} diff --git a/pylontech/__init__.py b/pylontech/__init__.py deleted file mode 100644 index d802427..0000000 --- a/pylontech/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .pylontech import Pylontech diff --git a/pylontech/pylontech.py b/pylontech/pylontech.py deleted file mode 100644 index 356c6cc..0000000 --- a/pylontech/pylontech.py +++ /dev/null @@ -1,308 +0,0 @@ -from typing import Dict -import logging -import serial -import construct - -logger = logging.getLogger(__name__) - -class HexToByte(construct.Adapter): - def _decode(self, obj, context, path) -> bytes: - hexstr = ''.join([chr(x) for x in obj]) - return bytes.fromhex(hexstr) - - -class JoinBytes(construct.Adapter): - def _decode(self, obj, context, path) -> bytes: - return ''.join([chr(x) for x in obj]).encode() - - -class DivideBy1000(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return obj / 1000 - - -class DivideBy100(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return obj / 100 - -class DivideBy10(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return obj / 10 - -class ToVolt(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return obj / 1000 - -class ToAmp(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return obj / 10 - -class ToCelsius(construct.Adapter): - def _decode(self, obj, context, path) -> float: - return (obj - 2731) / 10.0 # in Kelvin*10 - - - -class Pylontech: - manufacturer_info_fmt = construct.Struct( - "DeviceName" / JoinBytes(construct.Array(10, construct.Byte)), - "SoftwareVersion" / construct.Array(2, construct.Byte), - "ManufacturerName" / JoinBytes(construct.GreedyRange(construct.Byte)), - ) - - system_parameters_fmt = construct.Struct( - "CellHighVoltageLimit" / ToVolt(construct.Int16ub), - "CellLowVoltageLimit" / ToVolt(construct.Int16ub), - "CellUnderVoltageLimit" / ToVolt(construct.Int16sb), - "ChargeHighTemperatureLimit" / ToCelsius(construct.Int16sb), - "ChargeLowTemperatureLimit" / ToCelsius(construct.Int16sb), - "ChargeCurrentLimit" / DivideBy10(construct.Int16sb), - "ModuleHighVoltageLimit" / ToVolt(construct.Int16ub), - "ModuleLowVoltageLimit" / ToVolt(construct.Int16ub), - "ModuleUnderVoltageLimit" / ToVolt(construct.Int16ub), - "DischargeHighTemperatureLimit" / ToCelsius(construct.Int16sb), - "DischargeLowTemperatureLimit" / ToCelsius(construct.Int16sb), - "DischargeCurrentLimit" / DivideBy10(construct.Int16sb), - ) - - management_info_fmt = construct.Struct( - "ChargeVoltageLimit" / DivideBy1000(construct.Int16ub), - "DischargeVoltageLimit" / DivideBy1000(construct.Int16ub), - "ChargeCurrentLimit" / ToAmp(construct.Int16sb), - "DischargeCurrentLimit" / ToAmp(construct.Int16sb), - "status" - / construct.BitStruct( - "ChargeEnable" / construct.Flag, - "DischargeEnable" / construct.Flag, - "ChargeImmediately2" / construct.Flag, - "ChargeImmediately1" / construct.Flag, - "FullChargeRequest" / construct.Flag, - "ShouldCharge" - / construct.Computed( - lambda this: this.ChargeImmediately2 - | this.ChargeImmediately1 - | this.FullChargeRequest - ), - "_padding" / construct.BitsInteger(3), - ), - ) - - module_serial_number_fmt = construct.Struct( - "CommandValue" / construct.Byte, - "ModuleSerialNumber" / JoinBytes(construct.Array(16, construct.Byte)), - ) - - get_values_fmt = construct.Struct( - "NumberOfModules" / construct.Byte, - "Module" / construct.Array(construct.this.NumberOfModules, construct.Struct( - "NumberOfCells" / construct.Int8ub, - "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)), - "NumberOfTemperatures" / construct.Int8ub, - "AverageBMSTemperature" / ToCelsius(construct.Int16sb), - "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)), - "Current" / ToAmp(construct.Int16sb), - "Voltage" / ToVolt(construct.Int16ub), - "Power" / construct.Computed(construct.this.Current * construct.this.Voltage), - "_RemainingCapacity1" / DivideBy1000(construct.Int16ub), - "_UserDefinedItems" / construct.Int8ub, - "_TotalCapacity1" / DivideBy1000(construct.Int16ub), - "CycleNumber" / construct.Int16ub, - "_OptionalFields" / construct.If(construct.this._UserDefinedItems > 2, - construct.Struct("RemainingCapacity2" / DivideBy1000(construct.Int24ub), - "TotalCapacity2" / DivideBy1000(construct.Int24ub))), - "RemainingCapacity" / construct.Computed(lambda this: this._OptionalFields.RemainingCapacity2 if this._UserDefinedItems > 2 else this._RemainingCapacity1), - "TotalCapacity" / construct.Computed(lambda this: this._OptionalFields.TotalCapacity2 if this._UserDefinedItems > 2 else this._TotalCapacity1), - )), - "TotalPower" / construct.Computed(lambda this: sum([x.Power for x in this.Module])), - "StateOfCharge" / construct.Computed(lambda this: sum([x.RemainingCapacity for x in this.Module]) / sum([x.TotalCapacity for x in this.Module])), - - ) - get_values_single_fmt = construct.Struct( - "NumberOfModule" / construct.Byte, - "NumberOfCells" / construct.Int8ub, - "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)), - "NumberOfTemperatures" / construct.Int8ub, - "AverageBMSTemperature" / ToCelsius(construct.Int16sb), - "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)), - "Current" / ToAmp(construct.Int16sb), - "Voltage" / ToVolt(construct.Int16ub), - "Power" / construct.Computed(construct.this.Current * construct.this.Voltage), - "_RemainingCapacity1" / DivideBy1000(construct.Int16ub), - "_UserDefinedItems" / construct.Int8ub, - "_TotalCapacity1" / DivideBy1000(construct.Int16ub), - "CycleNumber" / construct.Int16ub, - "_OptionalFields" / construct.If(construct.this._UserDefinedItems > 2, - construct.Struct("RemainingCapacity2" / DivideBy1000(construct.Int24ub), - "TotalCapacity2" / DivideBy1000(construct.Int24ub))), - "RemainingCapacity" / construct.Computed(lambda this: this._OptionalFields.RemainingCapacity2 if this._UserDefinedItems > 2 else this._RemainingCapacity1), - "TotalCapacity" / construct.Computed(lambda this: this._OptionalFields.TotalCapacity2 if this._UserDefinedItems > 2 else this._TotalCapacity1), - "TotalPower" / construct.Computed(construct.this.Power), - "StateOfCharge" / construct.Computed(construct.this.RemainingCapacity / construct.this.TotalCapacity), - ) - - def __init__(self, serial_port='/dev/ttyUSB0', baudrate=115200): - self.s = serial.Serial(serial_port, baudrate, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=2, exclusive=True) - - - @staticmethod - def get_frame_checksum(frame: bytes): - assert isinstance(frame, bytes) - - sum = 0 - for byte in frame: - sum += byte - sum = ~sum - sum %= 0x10000 - sum += 1 - return sum - - @staticmethod - def get_info_length(info: bytes) -> int: - lenid = len(info) - if lenid == 0: - return 0 - - lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf) - lenid_modulo = lenid_sum % 16 - lenid_invert_plus_one = 0b1111 - lenid_modulo + 1 - - return (lenid_invert_plus_one << 12) + lenid - - - def send_cmd(self, address: int, cmd, info: bytes = b''): - raw_frame = self._encode_cmd(address, cmd, info) - self.s.write(raw_frame) - - - def _encode_cmd(self, address: int, cid2: int, info: bytes = b''): - cid1 = 0x46 - - info_length = Pylontech.get_info_length(info) - - frame = "{:02X}{:02X}{:02X}{:02X}{:04X}".format(0x20, address, cid1, cid2, info_length).encode() - frame += info - - frame_chksum = Pylontech.get_frame_checksum(frame) - whole_frame = (b"~" + frame + "{:04X}".format(frame_chksum).encode() + b"\r") - return whole_frame - - - def _decode_hw_frame(self, raw_frame: bytes) -> bytes: - # XXX construct - frame_data = raw_frame[1:len(raw_frame) - 5] - frame_chksum = raw_frame[len(raw_frame) - 5:-1] - - got_frame_checksum = Pylontech.get_frame_checksum(frame_data) - assert got_frame_checksum == int(frame_chksum, 16) - - return frame_data - - def _decode_frame(self, frame): - format = construct.Struct( - "ver" / HexToByte(construct.Array(2, construct.Byte)), - "adr" / HexToByte(construct.Array(2, construct.Byte)), - "cid1" / HexToByte(construct.Array(2, construct.Byte)), - "cid2" / HexToByte(construct.Array(2, construct.Byte)), - "infolength" / HexToByte(construct.Array(4, construct.Byte)), - "info" / HexToByte(construct.GreedyRange(construct.Byte)), - ) - - return format.parse(frame) - - - def read_frame(self): - raw_frame = self.s.readline() - f = self._decode_hw_frame(raw_frame=raw_frame) - parsed = self._decode_frame(f) - return parsed - - - def scan_for_batteries(self, start=0, end=255) -> Dict[int, str]: - """ Returns a map of the batteries id to their serial number """ - batteries = {} - for adr in range(start, end, 1): - bdevid = "{:02X}".format(adr).encode() - self.send_cmd(adr, 0x93, bdevid) # Probe for serial number - raw_frame = self.s.readline() - - if raw_frame: - sn = self.get_module_serial_number(adr) - sn_str = sn["ModuleSerialNumber"].decode() - - batteries[adr] = sn_str - logger.debug("Found battery at address " + str(adr) + " with serial " + sn_str) - else: - logger.debug("No battery found at address " + str(adr)) - - return batteries - - - def get_protocol_version(self): - self.send_cmd(0, 0x4f) - return self.read_frame() - - - def get_manufacturer_info(self): - self.send_cmd(0, 0x51) - f = self.read_frame() - return self.manufacturer_info_fmt.parse(f.info) - - - def get_system_parameters(self, dev_id=None): - if dev_id: - bdevid = "{:02X}".format(dev_id).encode() - self.send_cmd(dev_id, 0x47, bdevid) - else: - self.send_cmd(2, 0x47) - - f = self.read_frame() - return self.system_parameters_fmt.parse(f.info[1:]) - - def get_management_info(self, dev_id): - bdevid = "{:02X}".format(dev_id).encode() - self.send_cmd(dev_id, 0x92, bdevid) - f = self.read_frame() - - print(f.info) - print(len(f.info)) - ff = self.management_info_fmt.parse(f.info[1:]) - print(ff) - return ff - - def get_module_serial_number(self, dev_id=None): - if dev_id: - bdevid = "{:02X}".format(dev_id).encode() - self.send_cmd(dev_id, 0x93, bdevid) - else: - self.send_cmd(2, 0x93) - - f = self.read_frame() - # infoflag = f.info[0] - return self.module_serial_number_fmt.parse(f.info[0:]) - - def get_values(self): - self.send_cmd(2, 0x42, b'FF') - f = self.read_frame() - - # infoflag = f.info[0] - d = self.get_values_fmt.parse(f.info[1:]) - return d - - def get_values_single(self, dev_id): - bdevid = "{:02X}".format(dev_id).encode() - self.send_cmd(dev_id, 0x42, bdevid) - f = self.read_frame() - # infoflag = f.info[0] - d = self.get_values_single_fmt.parse(f.info[1:]) - return d - - -if __name__ == '__main__': - p = Pylontech() - # print(p.get_protocol_version()) - # print(p.get_manufacturer_info()) - # print(p.get_system_parameters()) - # print(p.get_management_info()) - # print(p.get_module_serial_number()) - # print(p.get_values()) - print(p.get_values_single(2)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cd629b1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[project] +name = "python-pylontech-ext" +version = "0.4.6" +description = "Interfaces with Pylontech Batteries using RS485 protocol" +authors = [ + { name = "Frank Villaro-Dixon", email = "frank@villaro-dixon.eu" }, + { name = "Pavel Shirshov", email = "pshirshov@eml.cc" }, +] +requires-python = ">=3.13" +readme = "README.md" +license = "MIT" +keywords = [ + "pylontech", + "pylon", + "rs485", + "lithium battery", + "US2000", + "US2000C", + "US3000", + "US5000", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Topic :: Utilities", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "pyserial", + "construct", + + "standard-telnetlib", + "Exscript", + + "rich", + "pymongo", + "requests", + + "paho-mqtt", + "ha-mqtt-discoverable", + +] +url = "http://github.com/Frankkkkk/python-pylontech" + +[project.scripts] +poller = "pylontechpoller:poller.main" + +[dependency-groups] +test = ["pytest"] +dev = ["flake8"] + +[tool.uv] +default-groups = [ + "test", + "dev", +] + +[tool.hatch.build.targets.sdist] +include = [ + "src/pylontech", + "src/pylontechpoller", +] +exclude = ["demos"] + +[tool.hatch.build.targets.wheel] +include = [ + "src/pylontech", + "src/pylontechpoller", +] +exclude = ["demos"] + +[tool.hatch.build.targets.wheel.sources] +"src/pylontech" = "pylontech" +"src/pylontechpoller" = "pylontechpoller" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pytest.ini_options] +testpaths = ["tests"] +log_cli_level = "INFO" +xfail_strict = true diff --git a/setup.py b/setup.py deleted file mode 100644 index 090419f..0000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from setuptools import setup - - -setup( - name="python-pylontech", - version="0.3.3", - author="Frank Villaro-Dixon", - author_email="frank@villaro-dixon.eu", - description=("Interfaces with Pylontech Batteries using RS485 protocol"), - license="MIT", - keywords="pylontech pylon rs485 lithium battery US2000 US2000C US3000", - url="http://github.com/Frankkkkk/python-pylontech", - packages=['pylontech'], - long_description=open("README.md", "r").read(), - long_description_content_type="text/markdown", - install_requires=['pyserial', 'construct'], - classifiers=[ - "Development Status :: 3 - Alpha", - "Topic :: Utilities", - "License :: OSI Approved :: MIT License", - ], -) diff --git a/src/pylontech/__init__.py b/src/pylontech/__init__.py new file mode 100644 index 0000000..e19ecc0 --- /dev/null +++ b/src/pylontech/__init__.py @@ -0,0 +1,7 @@ +from .pylontech import Pylontech + +from .transport import SerialTransport +from .transport import TelnetlibLegacyTransport +from .transport import ExscriptTelnetTransport +from .transport import SerialDeviceTransport +from .tools import to_json_serializable diff --git a/src/pylontech/pylontech.py b/src/pylontech/pylontech.py new file mode 100644 index 0000000..c2d74f6 --- /dev/null +++ b/src/pylontech/pylontech.py @@ -0,0 +1,111 @@ +import datetime + +from .transport import * +from .schema import * +from typing import * +import logging + +logger = logging.getLogger(__name__) + +class PylontechModule: + def __init__(self, idx, serial, manufacturer_info, device_name, system_parameters, management_info, fw_version): + self.idx = idx + self.serial = serial + self.manufacturer_info = manufacturer_info + self.device_name = device_name + self.system_parameters = system_parameters + self.management_info = management_info + self.fw_version = fw_version + +class PylontechStackData: + def __init__(self, modules: Dict[int, PylontechModule]): + self.ids = list(modules.keys()) + self.modules = modules + + def range(self): + return range(min(self.ids), max(self.ids)+1) + +class Pylontech(PylontechSchema): + def __init__(self, transport): + self.transport = transport + + def poll_parameters(self, ids: range): + while True: + result = {"timestamp": datetime.datetime.now(datetime.UTC), "modules": []} + for idx in ids: + vals = to_json_serializable(self.get_values_single(idx)) + result["modules"].append(vals) + yield result + + def scan_for_batteries(self, start=0, end=255) -> PylontechStackData: + """ Returns a map of the batteries id to their serial number """ + batteries = {} + for adr in range(start, end, 1): + self.transport.send_cmd(adr, 0x93, "{:02X}".format(adr).encode()) # Probe for serial number + raw_frame = self.transport.readln() + + if raw_frame: + sn = self.get_module_serial_number(adr) + sn_str = sn["ModuleSerialNumber"].decode() + + sp = self.get_system_parameters(adr) + mi = self.get_management_info(adr) + + m = self.get_manufacturer_info(adr) + nme = m["DeviceName"].decode() + mfr = m["ManufacturerName"].decode() + sw = m["SoftwareVersion"] + + batteries[adr] = PylontechModule(adr, sn_str, mfr, nme, sp, mi, sw) + + logger.debug("Found battery at address " + str(adr) + " with serial " + sn_str) + else: + logger.debug("No battery found at address " + str(adr)) + + return PylontechStackData(batteries) + + + def get_protocol_version(self, adr): + self.transport.send_cmd(adr, 0x4f, "{:02X}".format(adr).encode()) + return self.transport.read_frame() + + def get_manufacturer_info(self, adr): + self.transport.send_cmd(adr, 0x51, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + return self.manufacturer_info_fmt.parse(f.info) + + def get_system_parameters(self, adr): + self.transport.send_cmd(adr, 0x47, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + return self.system_parameters_fmt.parse(f.info[1:]) + + def get_management_info(self, adr): + self.transport.send_cmd(adr, 0x92, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + ff = self.management_info_fmt.parse(f.info[1:]) + return ff + + def get_module_serial_number(self, adr): + self.transport.send_cmd(adr, 0x93, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + return self.module_serial_number_fmt.parse(f.info[0:]) + + def get_module_software_version(self, adr): + self.transport.send_cmd(adr, 0x96, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + return self.module_software_version_fmt.parse(f.info) + + def get_values(self): + self.transport.send_cmd(2, 0x42, b'FF') + f = self.transport.read_frame() + return self.get_values_fmt.parse(f.info[1:]) + + def get_values_single(self, adr): + self.transport.send_cmd(adr, 0x42, "{:02X}".format(adr).encode()) + f = self.transport.read_frame() + return self.get_values_single_fmt.parse(f.info[1:]) + + def get_alarm_info(self, adr=0): + self.transport.send_cmd(adr, 0x4f,b'FF') + return self.transport.read_frame() + diff --git a/src/pylontech/schema.py b/src/pylontech/schema.py new file mode 100644 index 0000000..4dc5acf --- /dev/null +++ b/src/pylontech/schema.py @@ -0,0 +1,104 @@ +import construct +from .tools import * + +class PylontechSchema: + manufacturer_info_fmt = construct.Struct( + "DeviceName" / JoinBytes(construct.Array(10, construct.Byte)), + "SoftwareVersion" / construct.Array(2, construct.Byte), + "ManufacturerName" / JoinBytes(construct.GreedyRange(construct.Byte)), + ) + + system_parameters_fmt = construct.Struct( + "CellHighVoltageLimit" / ToVolt(construct.Int16ub), + "CellLowVoltageLimit" / ToVolt(construct.Int16ub), + "CellUnderVoltageLimit" / ToVolt(construct.Int16sb), + "ChargeHighTemperatureLimit" / ToCelsius(construct.Int16sb), + "ChargeLowTemperatureLimit" / ToCelsius(construct.Int16sb), + "ChargeCurrentLimit" / DivideBy10(construct.Int16sb), + "ModuleHighVoltageLimit" / ToVolt(construct.Int16ub), + "ModuleLowVoltageLimit" / ToVolt(construct.Int16ub), + "ModuleUnderVoltageLimit" / ToVolt(construct.Int16ub), + "DischargeHighTemperatureLimit" / ToCelsius(construct.Int16sb), + "DischargeLowTemperatureLimit" / ToCelsius(construct.Int16sb), + "DischargeCurrentLimit" / DivideBy10(construct.Int16sb), + ) + + management_info_fmt = construct.Struct( + "ChargeVoltageLimit" / DivideBy1000(construct.Int16ub), + "DischargeVoltageLimit" / DivideBy1000(construct.Int16ub), + "ChargeCurrentLimit" / ToAmp(construct.Int16sb), + "DischargeCurrentLimit" / ToAmp(construct.Int16sb), + "status" + / construct.BitStruct( + "ChargeEnable" / construct.Flag, + "DischargeEnable" / construct.Flag, + "ChargeImmediately2" / construct.Flag, + "ChargeImmediately1" / construct.Flag, + "FullChargeRequest" / construct.Flag, + "ShouldCharge" + / construct.Computed( + lambda this: this.ChargeImmediately2 + | this.ChargeImmediately1 + | this.FullChargeRequest + ), + "_padding" / construct.BitsInteger(3), + ), + ) + + module_serial_number_fmt = construct.Struct( + "CommandValue" / construct.Byte, + "ModuleSerialNumber" / JoinBytes(construct.Array(16, construct.Byte)), + ) + + module_software_version_fmt = construct.Struct( + "CommandValue" / construct.Byte, + "ModuleSoftwareVersion" / JoinBytes(construct.Array(5, construct.Byte)), + ) + + get_values_fmt = construct.Struct( + "NumberOfModules" / construct.Byte, + "Module" / construct.Array(construct.this.NumberOfModules, construct.Struct( + "NumberOfCells" / construct.Int8ub, + "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)), + "NumberOfTemperatures" / construct.Int8ub, + "AverageBMSTemperature" / ToCelsius(construct.Int16sb), + "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)), + "Current" / ToAmp(construct.Int16sb), + "Voltage" / ToVolt(construct.Int16ub), + "Power" / construct.Computed(construct.this.Current * construct.this.Voltage), + "_RemainingCapacity1" / DivideBy1000(construct.Int16ub), + "_UserDefinedItems" / construct.Int8ub, + "_TotalCapacity1" / DivideBy1000(construct.Int16ub), + "CycleNumber" / construct.Int16ub, + "_OptionalFields" / construct.If(construct.this._UserDefinedItems > 2, + construct.Struct("RemainingCapacity2" / DivideBy1000(construct.Int24ub), + "TotalCapacity2" / DivideBy1000(construct.Int24ub))), + "RemainingCapacity" / construct.Computed(lambda this: this._OptionalFields.RemainingCapacity2 if this._UserDefinedItems > 2 else this._RemainingCapacity1), + "TotalCapacity" / construct.Computed(lambda this: this._OptionalFields.TotalCapacity2 if this._UserDefinedItems > 2 else this._TotalCapacity1), + )), + "TotalPower" / construct.Computed(lambda this: sum([x.Power for x in this.Module])), + "StateOfCharge" / construct.Computed(lambda this: sum([x.RemainingCapacity for x in this.Module]) / sum([x.TotalCapacity for x in this.Module])), + + ) + get_values_single_fmt = construct.Struct( + "NumberOfModule" / construct.Byte, + "NumberOfCells" / construct.Int8ub, + "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)), + "NumberOfTemperatures" / construct.Int8ub, + "AverageBMSTemperature" / ToCelsius(construct.Int16sb), + "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)), + "Current" / ToAmp(construct.Int16sb), + "Voltage" / ToVolt(construct.Int16ub), + "Power" / construct.Computed(construct.this.Current * construct.this.Voltage), + "_RemainingCapacity1" / DivideBy1000(construct.Int16ub), + "_UserDefinedItems" / construct.Int8ub, + "_TotalCapacity1" / DivideBy1000(construct.Int16ub), + "CycleNumber" / construct.Int16ub, + "_OptionalFields" / construct.If(construct.this._UserDefinedItems > 2, + construct.Struct("RemainingCapacity2" / DivideBy1000(construct.Int24ub), + "TotalCapacity2" / DivideBy1000(construct.Int24ub))), + "RemainingCapacity" / construct.Computed(lambda this: this._OptionalFields.RemainingCapacity2 if this._UserDefinedItems > 2 else this._RemainingCapacity1), + "TotalCapacity" / construct.Computed(lambda this: this._OptionalFields.TotalCapacity2 if this._UserDefinedItems > 2 else this._TotalCapacity1), + "TotalPower" / construct.Computed(construct.this.Power), + "StateOfCharge" / construct.Computed(construct.this.RemainingCapacity / construct.this.TotalCapacity), + ) diff --git a/src/pylontech/tools.py b/src/pylontech/tools.py new file mode 100644 index 0000000..80aa084 --- /dev/null +++ b/src/pylontech/tools.py @@ -0,0 +1,58 @@ +import construct + + +class HexToByte(construct.Adapter): + def _decode(self, obj, context, path) -> bytes: + hexstr = ''.join([chr(x) for x in obj]) + return bytes.fromhex(hexstr) + + +class JoinBytes(construct.Adapter): + def _decode(self, obj, context, path) -> bytes: + return ''.join([chr(x) for x in obj]).encode() + + +class DivideBy1000(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return obj / 1000 + + +class DivideBy100(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return obj / 100 + +class DivideBy10(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return obj / 10 + +class ToVolt(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return obj / 1000 + +class ToAmp(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return obj / 10 + +class ToCelsius(construct.Adapter): + def _decode(self, obj, context, path) -> float: + return (obj - 2731) / 10.0 # in Kelvin*10 + +def to_json_serializable(obj): + from io import BytesIO + from construct import Container + import base64 + + if isinstance(obj, Container): + return {str(k): to_json_serializable(v) for k, v in obj.items() if k != "_io"} + elif isinstance(obj, dict): + return {str(k): to_json_serializable(v) for k, v in obj.items() if k != "_io"} + elif isinstance(obj, list): + return [to_json_serializable(v) for v in obj] + elif isinstance(obj, BytesIO): + return base64.b64encode(obj.getvalue()).decode('utf-8') # or use .hex() + elif isinstance(obj, bytes): + return base64.b64encode(obj).decode('utf-8') # or use obj.hex() + elif hasattr(obj, '__dict__'): + return {str(k): to_json_serializable(v) for k, v in vars(obj).items()} + else: + return obj diff --git a/src/pylontech/transport.py b/src/pylontech/transport.py new file mode 100644 index 0000000..8ec1afa --- /dev/null +++ b/src/pylontech/transport.py @@ -0,0 +1,155 @@ +import logging + +import serial +import telnetlib + +from .tools import * + +logger = logging.getLogger(__name__) + +class ChecksumMismatch(Exception): + def __init__(self, expected, actual): + self.expected = expected + self.actual = actual + super().__init__(self.__repr__()) + + def __repr__(self): + return f"expected {self.expected}, got {self.actual}" + +class FrameFormatException(Exception): + def __init__(self, raw_frame, message, cause = None): + self.raw_frame = raw_frame + self.cause = cause + self.message = message + super().__init__(self.__repr__()) + + def __repr__(self): + return self.message + + +class SerialTransport(): + def readln(self) -> bytes: + pass + + def write(self, data: bytes): + pass + + def send_cmd(self, address: int, cmd, info: bytes = b''): + raw_frame = self._encode_cmd(address, cmd, info) + self.write(raw_frame) + + def read_frame(self): + raw_frame = self.readln() + f = self._decode_hw_frame(raw_frame=raw_frame) + parsed = self._decode_frame(f) + return parsed + + def _encode_cmd(self, address: int, cid2: int, info: bytes = b''): + cid1 = 0x46 + + info_length = SerialTransport.get_info_length(info) + + frame = "{:02X}{:02X}{:02X}{:02X}{:04X}".format(0x20, address, cid1, cid2, info_length).encode() + frame += info + + frame_chksum = SerialTransport.get_frame_checksum(frame) + whole_frame = (b"~" + frame + "{:04X}".format(frame_chksum).encode() + b"\r") + return whole_frame + + + def _decode_hw_frame(self, raw_frame: bytes) -> bytes: + try: + frame_data = raw_frame[1:len(raw_frame) - 5] + frame_chksum = raw_frame[len(raw_frame) - 5:-1] + expected_frame_checksum = int(frame_chksum, 16) + real_frame_checksum = SerialTransport.get_frame_checksum(frame_data) + except BaseException as e: + m=f"cannot decode frame bytes, frame {raw_frame}" + raise FrameFormatException(raw_frame, message=m, cause=e) + + if real_frame_checksum != expected_frame_checksum: + m = f"expected checksum {expected_frame_checksum}, got {real_frame_checksum}, frame {raw_frame}" + raise FrameFormatException(raw_frame, message=m, cause=ChecksumMismatch(expected_frame_checksum, real_frame_checksum)) + + return frame_data + + @staticmethod + def get_frame_checksum(frame: bytes): + assert isinstance(frame, bytes) + + sum = 0 + for byte in frame: + sum += byte + sum = ~sum + sum %= 0x10000 + sum += 1 + return sum + + @staticmethod + def get_info_length(info: bytes) -> int: + lenid = len(info) + if lenid == 0: + return 0 + + lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf) + lenid_modulo = lenid_sum % 16 + lenid_invert_plus_one = 0b1111 - lenid_modulo + 1 + + return (lenid_invert_plus_one << 12) + lenid + + def _decode_frame(self, frame): + format = construct.Struct( + "ver" / HexToByte(construct.Array(2, construct.Byte)), + "adr" / HexToByte(construct.Array(2, construct.Byte)), + "cid1" / HexToByte(construct.Array(2, construct.Byte)), + "cid2" / HexToByte(construct.Array(2, construct.Byte)), + "infolength" / HexToByte(construct.Array(4, construct.Byte)), + "info" / HexToByte(construct.GreedyRange(construct.Byte)), + ) + + return format.parse(frame) + +class SerialDeviceTransport(SerialTransport): + def __init__(self, serial_port='/dev/ttyUSB0', baudrate=115200): + self.s = serial.Serial(serial_port, baudrate, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=2, exclusive=True) + + def readln(self) -> bytes: + return self.s.readline() + + def write(self, data: bytes): + self.s.write(data) + + +class TelnetlibLegacyTransport(SerialTransport): + def __init__(self, host, port=23, timeout=2): + self.timeout = timeout + self.s = telnetlib.Telnet(host, port, timeout=self.timeout) + + def readln(self) -> bytes: + return self.s.read_until(b'\r', timeout=self.timeout) + + def write(self, data: bytes): + self.s.write(data) + +from Exscript.protocols import Telnet + +class ExscriptTelnetTransport(SerialTransport): + def __init__(self, host, port=23, timeout=2): + self.timeout = timeout + self.conn = Telnet() + self.conn.connect(host, port) + self.conn.set_timeout(timeout) + + def readln(self): + data = b'' + while True: + chunk = self.conn.tn.rawq_getchar() + if not chunk: + break + data += chunk + if chunk == b'\r': + break + return data + + def write(self, data: bytes): + self.conn.send(data) diff --git a/src/pylontechpoller/__init__.py b/src/pylontechpoller/__init__.py new file mode 100644 index 0000000..2bcbec7 --- /dev/null +++ b/src/pylontechpoller/__init__.py @@ -0,0 +1 @@ +from .poller import main \ No newline at end of file diff --git a/src/pylontechpoller/hass_basic_reporter.py b/src/pylontechpoller/hass_basic_reporter.py new file mode 100644 index 0000000..68eca5d --- /dev/null +++ b/src/pylontechpoller/hass_basic_reporter.py @@ -0,0 +1,39 @@ +import json +import os + +import requests + +from pylontechpoller.reporter import Reporter, logger + + +class HassReporter(Reporter): + def __init__(self, hass_url, hass_stack_disbalance, hass_max_battery_disbalance, hass_max_battery_disbalance_id, hass_token): + self.hass_url = hass_url + self.hass_stack_disbalance = hass_stack_disbalance + self.hass_max_battery_disbalance = hass_max_battery_disbalance + self.hass_max_battery_disbalance_id = hass_max_battery_disbalance_id + if os.path.exists(hass_token): + with open(hass_token, 'r') as file: + hass_token = file.read().strip() + self.hass_token = hass_token + + + def report_state(self, state): + md = state["max_module_disbalance"] + self.update_hass_state(self.hass_stack_disbalance, int(state["stack_disbalance"] * 10000) / 10000.0) + self.update_hass_state(self.hass_max_battery_disbalance, int(md[1] * 10000) / 10000.0) + self.update_hass_state(self.hass_max_battery_disbalance_id, md[0]) + + def update_hass_state(self, id, value): + tpe = id.split('.')[0] + update = { + "entity_id": id, + "value": value + } + + url = f'{self.hass_url}/api/services/{tpe}/set_value' + + response = requests.post(url, data=json.dumps(update), headers={"Authorization": f"Bearer {self.hass_token}"}) + + if response.status_code != 200: + logger.error(f"hass state update failed for {id}: {response.status_code} {response.text}") diff --git a/src/pylontechpoller/mongo_reporter.py b/src/pylontechpoller/mongo_reporter.py new file mode 100644 index 0000000..3da32ae --- /dev/null +++ b/src/pylontechpoller/mongo_reporter.py @@ -0,0 +1,27 @@ +import datetime + +from pymongo import MongoClient + +from pylontech import to_json_serializable, Pylontech +from pylontech.pylontech import PylontechStackData +from pylontechpoller.reporter import Reporter + + +class MongoReporter(Reporter): + def __init__(self, mongo_url, mongo_db, mongo_collection_meta, mongo_collection_history, retention_days): + mongo = MongoClient(mongo_url) + db = mongo[mongo_db] + self.retention_days = retention_days + self.collection_meta = db[mongo_collection_meta] + self.collection_hist = db[mongo_collection_history] + self.collection_hist.create_index("ts", expireAfterSeconds=3600 * 24 * 90) + + def report_meta(self, meta: PylontechStackData, p: Pylontech): + self.collection_meta.insert_one({'ts': datetime.datetime.now().isoformat(), "stack": to_json_serializable(meta)}) + + def report_state(self, state): + self.collection_hist.insert_one(state) + + def cleanup(self): + threshold = datetime.datetime.now() - datetime.timedelta(days= self.retention_days) + self.collection_hist.delete_many({"ts": {"$lt": threshold}}) diff --git a/src/pylontechpoller/mqtt_reporter.py b/src/pylontechpoller/mqtt_reporter.py new file mode 100644 index 0000000..e00564d --- /dev/null +++ b/src/pylontechpoller/mqtt_reporter.py @@ -0,0 +1,169 @@ +import os.path + +from ha_mqtt_discoverable import Settings, DeviceInfo +from ha_mqtt_discoverable.sensors import SensorInfo, Sensor + +from pylontech.pylontech import PylontechModule, Pylontech, PylontechStackData +from pylontechpoller.tools import minimize +from pylontechpoller.reporter import Reporter + +import paho.mqtt.client as mqtt + + +class MqttReporter(Reporter): + def __init__(self, mqtt_host, mqtt_port, mqtt_login, mqtt_password): + if os.path.exists(mqtt_password): + with open(mqtt_password, 'r') as file: + mqtt_password = file.read().strip() + + client = mqtt.Client(client_id="pylontech-poller") + client.username_pw_set(mqtt_login, mqtt_password) + client.connect(mqtt_host, mqtt_port) + client.loop_start() + self.mqtt_settings = Settings.MQTT(client=client) + # client.enable_logger(logger) + + # self.mqtt_settings = Settings.MQTT(host=mqtt_host, port=mqtt_port, username=mqtt_login, password=mqtt_password, + # client_name="pylontech-poller") + + self.device_info = DeviceInfo(name="Pylontech Battery Stack", identifiers="pylontech_battery_stack") + + self.hass_stack_disbalance_info = SensorInfo( + name="Stack Disbalance", + device_class="voltage", + unique_id="stack_disbalance", + unit_of_measurement="V", + suggested_display_precision=3, + device=self.device_info, + icon="mdi:scale-unbalanced", + + ) + self.hass_stack_disbalance_settings = Settings(mqtt=self.mqtt_settings, entity=self.hass_stack_disbalance_info) + self.hass_stack_disbalance = Sensor(self.hass_stack_disbalance_settings) + + self.hass_max_battery_disbalance_info = SensorInfo( + name="Max Battery Disbalance", + device_class="voltage", + unique_id="max_battery_disbalance", + unit_of_measurement="V", + suggested_display_precision=3, + device=self.device_info, + icon="mdi:scale-unbalanced", + ) + self.hass_max_battery_disbalance_settings = Settings(mqtt=self.mqtt_settings, + entity=self.hass_max_battery_disbalance_info) + self.hass_max_battery_disbalance = Sensor(self.hass_max_battery_disbalance_settings) + + self.hass_max_disbalance_id = Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Max disbalance ID", + unique_id=f"max_battery_disbalance_id", + device=self.device_info, + icon="mdi:battery-alert", + + ))) + self.bats = {} + + def report_meta(self, meta: PylontechStackData, p: Pylontech): + moduledata = { m["n"] : m for m in minimize( next(p.poll_parameters(meta.range())) )["modules"]} + cells = {} + + for id in meta.ids: + m = meta.modules[id] + device_info = DeviceInfo( + name=f"Pylontech Battery {id}", + identifiers=[f"pylontech_battery_{m.serial}", f"pylontech_battery_{id}", ], + manufacturer=m.manufacturer_info, + sw_version=".".join([str(x) for x in m.fw_version]), + model=m.device_name + ) + mdata = moduledata[id] + for cn, c in enumerate(mdata["cv"]): + cells[f"cell_{cn}_voltage"] = Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name=f"Cell {cn} Voltage", + device_class="voltage", + unique_id=f"cell_voltage_{id}_{cn}", + unit_of_measurement="V", + suggested_display_precision=3, + device=device_info, + entity_category="diagnostic", + icon="mdi:gauge", + ))) + + self.bats[id] = { + "bat_soc": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="SoC", + device_class="battery", + unique_id=f"battery_soc_{id}", + unit_of_measurement="%", + suggested_display_precision=1, + device=device_info + ))), + "bat_disbalance": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Cell Disbalance", + device_class="voltage", + unique_id=f"battery_disbalance_{id}", + unit_of_measurement="V", + suggested_display_precision=3, + device=device_info, + icon="mdi:scale-unbalanced", + ))), + "bat_voltage": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Voltage", + device_class="voltage", + unique_id=f"battery_voltage_{id}", + unit_of_measurement="V", + suggested_display_precision=3, + device=device_info, + icon="mdi:gauge", + ))), + "bat_current": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Current", + device_class="current", + unique_id=f"battery_current_{id}", + unit_of_measurement="A", + suggested_display_precision=3, + device=device_info, + icon="mdi:current-dc", + ))), + "bat_power": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Power", + device_class="power", + unique_id=f"battery_power_{id}", + unit_of_measurement="W", + suggested_display_precision=2, + device=device_info, + icon="mdi:battery-charging", + ))), + "bat_cycle": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Cycle", + unique_id=f"battery_cycle_{id}", + device=device_info, + icon="mdi:battery-sync", + ))), + "bat_temp": Sensor(Settings(mqtt=self.mqtt_settings, entity=SensorInfo( + name="Temperature", + device_class="temperature", + unique_id=f"battery_temperature_{id}", + unit_of_measurement="C", + suggested_display_precision=1, + device=device_info, + ))), + } | cells + + def report_state(self, state): + md = state["max_module_disbalance"] + self.hass_stack_disbalance.set_state(state["stack_disbalance"]) + self.hass_max_battery_disbalance.set_state(md[1]) + self.hass_max_disbalance_id.set_state(md[0]) + + for b in state["modules"]: + s = self.bats[b["n"]] + s["bat_disbalance"].set_state(b["disbalance"]) + s["bat_voltage"].set_state(b["v"]) + s["bat_current"].set_state(b["current"]) + s["bat_soc"].set_state(int(b["soc"] * 1000) / 10.0) + s["bat_power"].set_state(b["pw"]) + s["bat_cycle"].set_state(b["cycle"]) + s["bat_temp"].set_state(b["tempavg"]) + for cn, c in enumerate(b["cv"]): + s[f"cell_{cn}_voltage"].set_state(c) \ No newline at end of file diff --git a/src/pylontechpoller/poller.py b/src/pylontechpoller/poller.py new file mode 100644 index 0000000..d69eb7b --- /dev/null +++ b/src/pylontechpoller/poller.py @@ -0,0 +1,137 @@ +import argparse +import logging +import sys +import time + +from pylontech import * +from pylontechpoller.mqtt_reporter import MqttReporter +from pylontechpoller.hass_basic_reporter import HassReporter +from pylontechpoller.mongo_reporter import MongoReporter +from pylontechpoller.tools import minimize + +logger = logging.getLogger(__name__) + + + + +def run(argv: list[str]): + parser = argparse.ArgumentParser(description="Pylontech RS485 poller") + + parser.add_argument("source_host", help="Telnet host") + + parser.add_argument("--source-port", help="Telnet host", default=23) + parser.add_argument("--timeout", type=int, help="timeout", default=2) + parser.add_argument("--interval", type=int, help="polling interval in msec", default=1000) + parser.add_argument("--retention-days", type=int, help="how long to retain history data", default=90) + parser.add_argument("--debug", type=bool, help="verbose output", default=False) + + parser.add_argument("--mongo-url", type=str, help="mongodb url", default=None) + parser.add_argument("--mongo-db", type=str, help="target mongo database", default="pylontech") + parser.add_argument("--mongo-collection-history", type=str, help="target mongo collection_hist for stack history", default="history") + parser.add_argument("--mongo-collection-meta", type=str, help="target mongo collection_hist for stack data", default="meta") + + parser.add_argument("--hass-url", type=str, help="hass url", default=None) + parser.add_argument("--hass-stack-disbalance", type=str, help="state id", default="input_number.stack_disbalance") + parser.add_argument("--hass-max-battery-disbalance", type=str, help="state id", default="input_number.max_bat_disbalance") + parser.add_argument("--hass-max-battery-disbalance-id", type=str, help="state id", default="input_text.max_disbalance_id") + parser.add_argument("--hass-token", type=str, help="hass token or token file", default="/var/run/agenix/hass-token") + + + parser.add_argument("--mqtt-host", type=str, help="mqtt host", default=None) + parser.add_argument("--mqtt-port", type=int, help="mqtt url", default=1883) + parser.add_argument("--mqtt-user", type=str, help="mqtt login", default="mqtt") + parser.add_argument("--mqtt-password", type=str, help="mqtt password or password file", default="/var/run/agenix/mqtt-user") + + + + args = parser.parse_args(argv[1:]) + + level = logging.DEBUG if args.debug else logging.INFO + logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=level) + + cc = 0 + errs = 0 + spinner = ['|', '/', '-', '\\'] + + reporters = [] + + while True: + try: + logging.debug("Preparing client...") + p = Pylontech(ExscriptTelnetTransport(host=args.source_host, port=args.source_port, timeout=args.timeout)) + + mongo_url = args.mongo_url + + if mongo_url: + reporters.append(MongoReporter( + mongo_url, + args.mongo_db, + args.mongo_collection_meta, + args.mongo_collection_history, + args.retention_days + )) + + hass_url = args.hass_url + + if hass_url: + reporters.append(HassReporter( + hass_url, + args.hass_stack_disbalance, + args.hass_max_battery_disbalance, + args.hass_max_battery_disbalance_id, + args.hass_token_file + )) + + mqtt_host = args.mqtt_host + + if mqtt_host: + reporters.append(MqttReporter( + mqtt_host, + args.mqtt_port, + args.mqtt_user, + args.mqtt_password, + )) + + logging.info("About to start polling...") + bats = p.scan_for_batteries(2, 10) + + logging.info("Have battery stack data") + + for reporter in reporters: + reporter.report_meta(bats, p) + + for b in p.poll_parameters(bats.range()): + cc += 1 + + if sys.stdout.isatty(): + sys.stdout.write('\r' + spinner[cc % len(spinner)]) + sys.stdout.flush() + + mb = minimize(b) + # print(print_json(json.dumps(minimize(b)))) + for reporter in reporters: + reporter.report_state(mb) + + if cc % 1000 == 0: + logging.info("Updates submitted since startup: %d", cc) + for reporter in reporters: + reporter.cleanup() + + time.sleep(args.interval / 1000.0) + errs = 0 + except (KeyboardInterrupt, SystemExit): + exit(0) + except BaseException as e: + errs += 1 + logging.error("Exception occured: %s", e) + if errs > 10: + logging.error("Too many exceptions in a row, exiting just in case") + exit(1) + else: + time.sleep(args.interval / 1000.0) +def main(): + import sys + run(sys.argv) + +if __name__ == "__main__": + main() diff --git a/src/pylontechpoller/reporter.py b/src/pylontechpoller/reporter.py new file mode 100644 index 0000000..9ad0479 --- /dev/null +++ b/src/pylontechpoller/reporter.py @@ -0,0 +1,18 @@ +import logging + +from pylontech import Pylontech +from pylontech.pylontech import PylontechStackData + +logger = logging.getLogger(__name__) + +class Reporter: + def report_meta(self, meta: PylontechStackData, p: Pylontech): + pass + + def report_state(self, state): + pass + + def cleanup(self): + pass + + diff --git a/src/pylontechpoller/tools.py b/src/pylontechpoller/tools.py new file mode 100644 index 0000000..076fe1c --- /dev/null +++ b/src/pylontechpoller/tools.py @@ -0,0 +1,55 @@ +import json + + +def find_min_max_modules(modules): + all_voltages = [] + all_disbalances = [] + + for module in modules: + mid = module["NumberOfModule"] + cvs = module["CellVoltages"] + for voltage in cvs: + all_voltages.append((mid, voltage)) + vmax = max(cvs) + vmin = min(cvs) + d = vmax - vmin + all_disbalances.append((mid, d)) + + if not all_voltages: + return None, None + + min_pair = min(all_voltages, key=lambda x: x[1]) + max_pair = max(all_voltages, key=lambda x: x[1]) + max_disbalance = max(all_disbalances, key=lambda x: abs(x[1])) + + return min_pair, max_pair, max_disbalance + +def minimize(b: json) -> json: + def minimize_module(m: json) -> json: + return { + "n": m["NumberOfModule"], + "v": m["Voltage"], + "cv": m["CellVoltages"], + "current": m["Current"], + "pw": m["Power"], + "cycle": m["CycleNumber"], + "soc": m["StateOfCharge"], + "tempavg": m["AverageBMSTemperature"], + "temps": m["GroupedCellsTemperatures"], + "remaining": m["RemainingCapacity"], + "disbalance": max(m["CellVoltages"]) - min(m["CellVoltages"]) + } + + modules = b["modules"] + find_min_max_modules(modules) + + (min_pair, max_pair, max_disbalance) = find_min_max_modules(modules) + + return { + "ts": b["timestamp"], + "cvmin": min_pair, + "cvmax": max_pair, + "stack_disbalance": max_pair[1] - min_pair[1], + "max_module_disbalance": max_disbalance, + "modules": list(map(minimize_module, modules)), + } diff --git a/tests/test_basic.py b/tests/test_basic.py index 0a26da6..619d897 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,11 +2,11 @@ from typing import List from pytest import approx +from pylontech import SerialTransport + sys.path.extend("..") import pylontech -from pylontech.pylontech import ToVolt, ToAmp, ToCelsius, DivideBy1000 -import construct class MockSerial(object): @@ -23,9 +23,20 @@ def write(self, data: bytes): print(f"write: {data}") +class MockTransport(SerialTransport): + def __init__(self, responses: List[bytes]): + self.s = MockSerial(responses) + + def readln(self) -> bytes: + return self.s.readline() + + def write(self, data: bytes): + self.s.write(data) + + class Pylontech(pylontech.Pylontech): def __init__(self, responses): - self.s = MockSerial(responses) + super().__init__(MockTransport(responses)) def test_us2000_3modules_info_parsing_1(): diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..88214fe --- /dev/null +++ b/uv.lock @@ -0,0 +1,655 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "configparser" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/ac/ea19242153b5e8be412a726a70e82c7b5c1537c83f61b20995b2eda3dcd7/configparser-7.2.0.tar.gz", hash = "sha256:b629cc8ae916e3afbd36d1b3d093f34193d851e11998920fdcfc4552218b7b70", size = 51273, upload-time = "2025-03-08T16:04:09.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/fe/f61e7129e9e689d9e40bbf8a36fb90f04eceb477f4617c02c6a18463e81f/configparser-7.2.0-py3-none-any.whl", hash = "sha256:fee5e1f3db4156dcd0ed95bc4edfa3580475537711f67a819c966b389d09ce62", size = 17232, upload-time = "2025-03-08T16:04:07.743Z" }, +] + +[[package]] +name = "construct" +version = "2.10.70" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/77/8c84b98eca70d245a2a956452f21d57930d22ab88cbeed9290ca630cf03f/construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", size = 86337, upload-time = "2023-11-29T08:44:49.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020, upload-time = "2023-11-29T08:44:46.876Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" }, + { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" }, + { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" }, + { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" }, + { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "exscript" +version = "2.6.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "configparser" }, + { name = "future" }, + { name = "paramiko" }, + { name = "pycryptodomex" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/bc/7a782226a5113e617149e850e5940eb01e42584c94d92153d41ded387361/Exscript-2.6.28-py2.py3-none-any.whl", hash = "sha256:85c061e6e6ab6ec30ec5dd5cf2375def405721f7c8b76935b6234faf196bd622", size = 255128, upload-time = "2023-03-08T23:06:15.187Z" }, +] + +[[package]] +name = "flake8" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177, upload-time = "2025-03-29T20:08:39.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786, upload-time = "2025-03-29T20:08:37.902Z" }, +] + +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + +[[package]] +name = "gitlike-commands" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/30/ad1e2fc1cb3fd55aa1549c6151c1a3ddb55c061bcde5419f3d12ff5120cd/gitlike_commands-0.3.0.tar.gz", hash = "sha256:72f4e65239cb6a4a2c614867c5f914b5d5994edd2863335515b543689b01ff70", size = 6736, upload-time = "2024-01-26T23:31:49.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d7/25dbc939f4f707f33b7743949648923271af660bd56f77bda40b47f031e0/gitlike_commands-0.3.0-py3-none-any.whl", hash = "sha256:c262f8f532639ec8558369bdc2cd904bd0b65638834ed333c42a51be69578f21", size = 7512, upload-time = "2024-01-26T23:31:47.856Z" }, +] + +[[package]] +name = "ha-mqtt-discoverable" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitlike-commands" }, + { name = "paho-mqtt" }, + { name = "pyaml" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/0d/c0bd1ced3e9a915ae6708478de5a15409857d23022a293c8cb1c221f4546/ha_mqtt_discoverable-0.19.2.tar.gz", hash = "sha256:2c0facdfdff5573a4bae7ab40e9b66cc077e65445fb9d6f356e1c74ce00aa9d9", size = 28631, upload-time = "2025-05-31T16:36:56.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/41/1b9e020548c37bb506bf5b7ba2205585caafed691eaf17f650beb87681fe/ha_mqtt_discoverable-0.19.2-py3-none-any.whl", hash = "sha256:84725816a53d4e64f9d81cac6493c60dbd73899300c169b978fff9fdcbae2344", size = 27764, upload-time = "2025-05-31T16:36:55.673Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "paramiko" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyaml" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/40/94f10f32ab952c5cca713d9ac9d8b2fdc37392d90eea403823eeac674c24/pyaml-25.5.0.tar.gz", hash = "sha256:5799560c7b1c9daf35a7a4535f53e2c30323f74cbd7cb4f2e715b16dd681a58a", size = 29812, upload-time = "2025-05-29T05:34:05.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/7d/1b5061beff826f902285827261485a058b943332eba8a5532a0164735205/pyaml-25.5.0-py3-none-any.whl", hash = "sha256:b9e0c4e58a5e8003f8f18e802db49fd0563ada587209b13e429bdcbefa87d035", size = 26422, upload-time = "2025-05-29T05:34:03.594Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pymongo" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/0c/1fb60383ab4b20566407b87f1a95b7f5cda83e8d5594da6fc84e2a543405/pymongo-4.13.0.tar.gz", hash = "sha256:92a06e3709e3c7e50820d352d3d4e60015406bcba69808937dac2a6d22226fde", size = 2166443, upload-time = "2025-05-14T19:11:08.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/9afa6091bce4adad7cad736dcdc35c139a9b551fc61032ef20c7ba17eae5/pymongo-4.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92f5e75ae265e798be1a8a40a29e2ab934e156f3827ca0e1c47e69d43f4dcb31", size = 965996, upload-time = "2025-05-14T19:10:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/e4242abffc0ee1501bb426d8a540e712e4f917491735f18622838b17f5a1/pymongo-4.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d631d879e934b46222f5092d8951cbb9fe83542649697c8d342ea7b5479f118", size = 965702, upload-time = "2025-05-14T19:10:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/0732876b48b1285bada803f4b0d7da5b720cf8f778d2117bbed9e04473a3/pymongo-4.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be048fb78e165243272a8cdbeb40d53eace82424b95417ab3ab6ec8e9b00c59b", size = 1953825, upload-time = "2025-05-14T19:10:16.214Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3b/6713fed92cab64508a1fb8359397c0720202e5f36d7faf4ed71b05875180/pymongo-4.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81d159bd23d8ac53a6e819cccee991cb9350ab2541dfaa25aeb2f712d23b0a5", size = 2031179, upload-time = "2025-05-14T19:10:18.307Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/1aad904563c312a0dc2ff752acf0f11194f836304d6e15d05dff3a33df08/pymongo-4.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af08ba2886f08d334bc7e5d5c662c60ea2f16e813a2c35106f399463fa11087", size = 1995093, upload-time = "2025-05-14T19:10:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/4c/cc/33786f4ce9a46c776f0d32601b353f8c42b552ea9ff8060c290c912b661e/pymongo-4.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b91f59137e46cd3ff17d5684a18e8006d65d0ee62eb1068b512262d1c2c5ae8", size = 1955820, upload-time = "2025-05-14T19:10:21.788Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dd/9a2a87bd4aab12a2281ac20d179912eed824cc6f67df49edd87fa4879b3e/pymongo-4.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61733c8f1ded90ab671a08033ee99b837073c73e505b3b3b633a55a0326e77f4", size = 1905394, upload-time = "2025-05-14T19:10:23.684Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/0a70db5e4c4e1c162207e31eaa3debf98476e0265b154f6d2252f85969b0/pymongo-4.13.0-cp313-cp313-win32.whl", hash = "sha256:d10d3967e87c21869f084af5716d02626a17f6f9ccc9379fcbece5821c2a9fb4", size = 926840, upload-time = "2025-05-14T19:10:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/fb104175a7f15dd69691c8c32bd4b99c4338ec89fe094b6895c940cf2afb/pymongo-4.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9fe172e93551ddfdb94b9ad34dccebc4b7b680dc1d131bc6bd661c4a5b2945c", size = 949383, upload-time = "2025-05-14T19:10:27.234Z" }, + { url = "https://files.pythonhosted.org/packages/62/3f/c89a6121b0143fde431f04c267a0d49159b499f518630a43aa6288709749/pymongo-4.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5adc1349fd5c94d5dfbcbd1ad9858d1df61945a07f5905dcf17bb62eb4c81f93", size = 1022500, upload-time = "2025-05-14T19:10:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/89/8fc36b83768b44805dd3a1caf755f019b110d2111671950b39c8c7781cd9/pymongo-4.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8e11ea726ff8ddc8c8393895cd7e93a57e2558c27273d3712797895c53d25692", size = 1022503, upload-time = "2025-05-14T19:10:30.757Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/f216cf6218f8ceb4025fd10e3de486553bd5373c3b71a45fef3483b745bb/pymongo-4.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02160ab3a67eca393a2a2bb83dccddf4db2196d0d7c6a980a55157e4bdadc06", size = 2282184, upload-time = "2025-05-14T19:10:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/56/32/08a9045dbcd76a25d36a0bd42c635b56d9aed47126bcca0e630a63e08444/pymongo-4.13.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fca24e4df05501420b2ce2207c03f21fcbdfac1e3f41e312e61b8f416c5b4963", size = 2369224, upload-time = "2025-05-14T19:10:34.942Z" }, + { url = "https://files.pythonhosted.org/packages/16/63/7991853fa6cf5e52222f8f480081840fb452d78c1dcd6803cabe2d3557a6/pymongo-4.13.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50c503b7e809e54740704ec4c87a0f2ccdb910c3b1d36c07dbd2029b6eaa6a50", size = 2328611, upload-time = "2025-05-14T19:10:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/e9/0f/11beecc8d48c7549db3f13f2101fd1c06ccb668697d33a6a5a05bb955574/pymongo-4.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66800de4f4487e7c437991b44bc1e717aadaf06e67451a760efe5cd81ce86575", size = 2279806, upload-time = "2025-05-14T19:10:38.652Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/0358efc8dba796545e9bd4642d1337a9b67b60928c583799fb0726594855/pymongo-4.13.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82c36928c1c26580ce4f2497a6875968636e87c77108ff253d76b1355181a405", size = 2219131, upload-time = "2025-05-14T19:10:40.444Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/373cd1cd21eff769e22e4e0924dcbfd770dfa1298566d51a7097857267fc/pymongo-4.13.0-cp313-cp313t-win32.whl", hash = "sha256:1397eac713b84946210ab556666cfdd787eee824e910fbbe661d147e110ec516", size = 975711, upload-time = "2025-05-14T19:10:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/1e204091bdf264a0d9eccc21f7da099903a7a30045f055a91178686c0259/pymongo-4.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:99a52cfbf31579cc63c926048cd0ada6f96c98c1c4c211356193e07418e6207c", size = 1004287, upload-time = "2025-05-14T19:10:45.468Z" }, +] + +[[package]] +name = "pynacl" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "python-pylontech-ext" +version = "0.4.6" +source = { editable = "." } +dependencies = [ + { name = "construct" }, + { name = "exscript" }, + { name = "ha-mqtt-discoverable" }, + { name = "paho-mqtt" }, + { name = "pymongo" }, + { name = "pyserial" }, + { name = "requests" }, + { name = "rich" }, + { name = "standard-telnetlib" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8" }, +] +test = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "construct" }, + { name = "exscript" }, + { name = "ha-mqtt-discoverable" }, + { name = "paho-mqtt" }, + { name = "pymongo" }, + { name = "pyserial" }, + { name = "requests" }, + { name = "rich" }, + { name = "standard-telnetlib" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "flake8" }] +test = [{ name = "pytest" }] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "standard-telnetlib" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +]