├── .dockerignore ├── .github └── workflows │ ├── docker.yaml │ └── python-publish.yml ├── .gitignore ├── API.md ├── Dockerfile ├── LICENSE ├── Manifest.in ├── README.md ├── ShellCommands.md ├── ShellEnvVariables.md ├── changelog.txt ├── docker-compose.yml ├── entrypoint.sh ├── example.py ├── gen-apidocs.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── wattpilot │ ├── __init__.py │ ├── ressources │ ├── __init__.py │ └── wattpilot.yaml │ └── wattpilotshell.py └── test-shell.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.xml 3 | *.json 4 | *.tar.gz 5 | 6 | 7 | # Setuptools distribution folder. 8 | /dist/ 9 | 10 | /build/ 11 | 12 | # Python egg metadata, regenerated from source files by setuptools. 13 | *.egg-info 14 | /test.py 15 | 16 | #vscode 17 | /.vscode/ 18 | 19 | unignore 20 | work 21 | .env 22 | dev.env 23 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/en/actions/publishing-packages/publishing-docker-images 2 | 3 | name: Build and publish Docker image 4 | 5 | on: 6 | push: 7 | branches: ['main'] 8 | tags: [ v* ] 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | build-and-push-image: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v2 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v2 30 | 31 | - name: Log in to the Container registry 32 | uses: docker/login-action@v2 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Extract metadata (tags, labels) for Docker 39 | id: meta 40 | uses: docker/metadata-action@v4 41 | with: 42 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v3 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | platforms: | 52 | linux/amd64 53 | linux/arm/v7 54 | linux/arm64/v8 55 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python distribution to PyPI Test 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-n-publish: 9 | name: Build and publish Python to PyPI 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | 15 | - name: Set up Python 3.10 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.10.4 19 | 20 | - name: Install pypa/build 21 | run: >- 22 | python -m 23 | pip install 24 | build 25 | --user 26 | 27 | - name: Build a binary wheel and a source tarball 28 | run: >- 29 | python -m 30 | build 31 | --sdist 32 | --wheel 33 | --outdir dist/ 34 | . 35 | 36 | - name: Publish distribution to Test PyPI 37 | uses: pypa/gh-action-pypi-publish@master 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 41 | repository_url: https://test.pypi.org/legacy/ 42 | 43 | 44 | - name: Publish distribution to PyPI 45 | uses: pypa/gh-action-pypi-publish@master 46 | with: 47 | user: __token__ 48 | password: ${{ secrets.PYPI_API_TOKEN }} 49 | 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.xml 3 | *.json 4 | *.tar.gz 5 | 6 | 7 | # Setuptools distribution folder. 8 | /dist/ 9 | 10 | /build/ 11 | 12 | # Python egg metadata, regenerated from source files by setuptools. 13 | *.egg-info 14 | /test.py 15 | 16 | #vscode 17 | /.vscode/ 18 | 19 | unignore 20 | work 21 | .env 22 | dev.env 23 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Wattpilot API Description 2 | 3 | ## WebSocket Message Types 4 | 5 | | Key | Title | Description | Example | 6 | |-----|-------|-------------|---------| 7 | | `hello` | Hello Message | Received upon connection before authentication | `{"type": "hello", "serial": "", "hostname": "Wattpilot_", "friendly_name": "", "manufacturer": "fronius", "devicetype": "wattpilot", "version": "36.3", "protocol": 2, "secured": true}` | 8 | | `authRequired` | Auth Required | Received after hello to ask for authentication | `{"type": "authRequired", "token1": "", "token2": ""}` | 9 | | `auth` | Auth Message | This message is sent from the client to Wattpilot to perform an authentication. | `{"type": "auth", "token3": "", "hash": ""}` | 10 | | `authSuccess` | Auth Success | Received after sending a correct authentication message | `{"type": "authSuccess", "token3": "", "hash": ""}` | 11 | | `authError` | Auth Error | Received after sending an incorrect authentication message (e.g. wrong password) | `{"type": "authError", "token3": "", "hash": "", "message": "Wrong password"}` | 12 | | `fullStatus` | Full Status | Set of messages received after successful connection. These messages contain all properties of Wattpilot. `partial:true` means that more `fullStatus` messages will be sent with additional properties. | `{"type": "fullStatus", "partial": true, "status": {"mod": 1, "rfb": 1698, "stao": null, "alw": true, "acu": 6, "acui": 6, "adi": true, "dwo": null, "tpa": 0}}` | 13 | | `deltaStatus` | Delta Status | Whenever a property changes a Delta Status is sent | `{"type": "deltaStatus", "status": {"rfb": 1699, "utc": "2022-04-22T10:44:08.865.407", "loc": "2022-04-22T12:44:08.866.280 +02:00", "rbt": 1560937433, "nrg": [236, 235, 235, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "fhz": 49.937, "rcd": 5, "lps": 61, "tpcm": [4, 0, 3, 1, 0, 0, 0, 0, 45, 0, 3, 0, 0, 47, 44, 0, 0, 0, 0, 0, 12]}}` | 14 | | `clearInverters` | Clear Inverters | Unknown | `{"type": "clearInverters", "partial": true}` | 15 | | `updateInverter` | Update Inverter | Contains information of connected PV inverter / power meter | `{"type": "updateInverter", "partial": false, "id": "", "paired": true, "deviceFamily": "DataManager", "label": "", "model": "PILOT", "commonName": "pilot-0.5e-", "ip": "", "connected": true, "reachableMdns": true, "reachableUdp": true, "reachableHttp": true, "status": 0, "message": "ok"}` | 16 | | `securedMsg` | Secured Message | Is sent by the client to change a property value. | `{"type": "securedMsg", "data": "{\"type\": \"setValue\", \"requestId\": 1, \"key\": \"nmo\", \"value\": true}", "requestId": "1sm", "hmac": ""}` | 17 | | `response` | Update Response Message | Received after sending an update and contains the result of the update | `{"type": "response", "requestId": "1", "success": true, "status": {"nmo": true}}` | 18 | 19 | ## WebSocket API Properties 20 | 21 | | Key/Alias | Title | R/W | JSON/API Type | Category | HA Enabled | Description | Example | 22 | |-----------|-------|-----|---------------|----------|------------|-------------|---------| 23 | | `abm`
- | | | `string`
- | | :white_large_square: | | `""` | 24 | | `acs`
`accessState` | Access State | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | access_control user setting (Open=0, Wait=1) | `0` | 25 | | `acu`
`allowedCurrent` | Allowed Current | R | `integer`
`int` | Status | :white_large_square: | How many ampere is the car allowed to charge now? | `6` | 26 | | `acui`
- | | | `integer`
- | | :white_large_square: | | `6` | 27 | | `adi`
`adapterLimit` | Adapter (16A) Limit | R | `boolean`
`bool` | Status | :white_large_square: | Is the 16A adapter used? Limits the current to 16A | `true` | 28 | | `al1`
`adapterLimit1` | Adapter Limit 1 | | `integer`
- | | :white_large_square: | | `6` | 29 | | `al2`
`adapterLimit2` | Adapter Limit 2 | | `integer`
- | | :white_large_square: | | `10` | 30 | | `al3`
`adapterLimit3` | Adapter Limit 3 | | `integer`
- | | :white_large_square: | | `12` | 31 | | `al4`
`adapterLimit4` | Adapter Limit 4 | | `integer`
- | | :white_large_square: | | `14` | 32 | | `al5`
`adapterLimit5` | Adapter Limit 5 | | `integer`
- | | :white_large_square: | | `16` | 33 | | `alw`
`allowCharging` | Allow Charging | R | `boolean`
`bool` | Status | :heavy_check_mark: | Is the car allowed to charge at all now? | `true` | 34 | | `ama`
`maxCurrentLimit` | Max Current Limit | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | ampere_max limit | `16` | 35 | | `amp`
`chargingCurrent` | Charging Current | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | requestedCurrent in Ampere, used for display on LED ring and logic calculations | `6` | 36 | | `amt`
`temperatureCurrentLimit` | Temperature Current Limit | R | `integer`
`int` | Status | :white_large_square: | temperatureCurrentLimit | `32` | 37 | | `apd`
`firmwareDescription` | Firmware Description | R | `object`
`object` | Constant | :white_large_square: | firmware description | `{"project_name": "wattpilot_hw4+", "version": "36.3", "secure_version": 0, "timestamp": "Jan 31 2022 22:51:39", "idf_ver": "v5.0-dev-1103-ga9ef558d", "sha256": ""}` | 38 | | `arv`
`appRecommendedVersion` | App Recommended Version | R | `string`
`string` | Constant | :white_large_square: | app recommended version (used to show in the app that the app is outdated) | `"1.2.1"` | 39 | | `asc`
- | | | `boolean`
- | | :white_large_square: | | `true` | 40 | | `aup`
- | | | `integer`
- | | :white_large_square: | | `6` | 41 | | `awc`
`awattarCountry` | Awattar Country | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | awattar country (Austria=0, Germany=1) | `0` | 42 | | `awcp`
`awattarCurrentPrice` | Awattar Current Price | R | `object`
`optional` | Status | :white_large_square: | awattar current price | `{"start": 1650567600, "end": 1650571200, "marketprice": 22.044}` | 43 | | `awp`
`awattarMaxPrice` | Awattar Max Price | R/W | `float`
`float` | Config | :heavy_check_mark: | awattarMaxPrice in ct | `3` | 44 | | `awpl`
`awattarPriceList` | Awattar Price List | R/W | `array`
`array` | Status | :white_large_square: | awattar price list, timestamps are measured in unix-time, seconds since 1970 | `[{"start": 1650567600, "end": 1650571200, "marketprice": 22.044}, {"start": 1650571200, "end": 1650574800, "marketprice": 19.971}]` | 45 | | `bac`
`buttonAllowCurrentChange` | Button Allow Current Change | R/W | `boolean`
`bool` | Config | :heavy_check_mark: | Button allow Current change | `true` | 46 | | `bam`
- | | | `boolean`
- | | :white_large_square: | | `true` | 47 | | `cae`
`cloudApiEnable` | Cloud API Enable | R/W | `boolean`
`bool` | | :white_large_square: | Set to true, if you want to use the go-eCharger API at https://.api.v3.go-e.io/api. | `false` | 48 | | `cak`
`cloudApiKey` | Cloud API Key | R/W | `string`
`string` | | :white_large_square: | Contains the access token (32 characters) for the cloud API. Whenever you write anything to cak a new token is generated automatically. | `"Cjk4BWbH8EcTFtY6dsWvTYz6CjBxifeO"` | 49 | | `car`
`carState` | Car State | R | `integer`
`optional` | Status | :heavy_check_mark: | carState, null if internal error (Unknown/Error=0, Idle=1, Charging=2, WaitCar=3, Complete=4, Error=5) | `1` | 50 | | `cards`
`registeredCards` | Registered Cards | R/W | `array`
`array` | | :white_large_square: | Registered RFID cards for different users | `[{"name": "User 1", "energy": 0, "cardId": true}, {"name": "User 2", "energy": 0, "cardId": false}]` | 51 | | `cbl`
`cableCurrentLimit` | Cable Current Limit | R | `integer`
`optional` | Status | :heavy_check_mark: | cable_current_limit in A | `20` | 52 | | `cbm`
- | | | `unknown`
- | | :white_large_square: | | `null` | 53 | | `cca`
`cloudClientAuth` | Cloud Client Auth | R | `boolean`
`bool` | Config | :white_large_square: | cloud websocket use client auth (if key&cert are setup correctly) | `true` | 54 | | `cch`
`colorCharging` | Color Charging | R/W | `string`
`string` | Config | :heavy_check_mark: | color_charging, format: #RRGGBB | `"#00FFF"` | 55 | | `cci`
- | | | `object`
- | | :white_large_square: | | `{"id": "", "paired": true, "deviceFamily": "DataManager", "label": "", "model": "PILOT", "commonName": "pilot-0.5e-", "ip": "", "connected": true, "reachableMdns": true, "reachableUdp": true, "reachableHttp": true, "status": 0, "message": "ok"}` | 56 | | `cco`
`carConsumption` | Car Consumption | R/W | `float`
`double` | Config | :heavy_check_mark: | car consumption in kWh for 100km (only stored for app) | `24` | 57 | | `ccrv`
`chargeControllerRecommendedVersion` | Charge Controller Recommended Version | R | `string`
`string` | Constant | :white_large_square: | | | 58 | | `ccu`
`chargeControllerUpdateProgress` | Charge Controller Update Progress | R | `object`
`optional` | Status | :white_large_square: | charge controller update progress (null if no update is in progress) | `null` | 59 | | `ccw`
`currentlyConnectedWifi` | Currently Connected Wifi | R | `object`
`optional` | Status | :white_large_square: | Currently connected WiFi | `{"ssid": "", "encryptionType": 3, "pairwiseCipher": 4, "groupCipher": 4, "b": true, "g": true, "n": true, "lr": false, "wps": false, "ftmResponder": false, "ftmInitiator": false, "channel": 6, "bssid": "", "ip": "", "netmask": "255.255.255.0", "gw": "", "ipv6": ["", ""], "dns0": "", "dns1": "0.0.0.0", "dns2": "0.0.0.0"}` | 60 | | `cdi`
`chargingDurationInfo` | Charging Duration Info | R | `object`
`object` | Status | :heavy_check_mark: | charging duration info (null=no charging in progress, type=0 counter going up, type=1 duration in ms) | `{"type": 1, "value": 11554770}` | 61 | | `cdv`
- | | | `unknown`
- | | :white_large_square: | | `null` | 62 | | `cfi`
`colorFinished` | Color Finished | R/W | `string`
`string` | Config | :white_large_square: | color_finished, format: #RRGGBB | `"#00FF00"` | 63 | | `chr`
- | | | `boolean`
- | | :white_large_square: | | `true` | 64 | | `cid`
`colorIdle` | Color Idle | R/W | `string`
`string` | Config | :white_large_square: | color_idle, format: #RRGGBB | `"#0000FF"` | 65 | | `clp`
`currentLimitPresets` | Current Limit Presets | R/W | `array`
`array` | Config | :white_large_square: | current limit presets, max. 5 entries | `[6, 10, 12, 14, 16]` | 66 | | `cpe`
`cpEnable` | CP Enable | R | `boolean`
`bool` | Status | :white_large_square: | The charge ctrl requests the CP signal enabled or not immediately | `true` | 67 | | `cpr`
`cpEnableRequest` | CP Enable Request | R | `boolean`
`bool` | Status | :white_large_square: | CP enable request. cpd=0 triggers the charge ctrl to set cpe=0 once processed | `true` | 68 | | `csca`
- | | | `integer`
- | | :white_large_square: | | `2` | 69 | | `ct`
`carType` | Car Type | R/W | `string`
`string` | Config | :white_large_square: | car type, free text string (max. 64 characters, only stored for app) | `"vwID3_4"` | 70 | | `cus`
`cableUnlockStatus` | Cable Unlock Status | R | `integer`
`uint8` | Status | :heavy_check_mark: | Cable unlock status (Unknown=0, Unlocked=1, UnlockFailed=2, Locked=3, LockFailed=4, LockUnlockPowerout=5) | `1` | 71 | | `cwc`
`colorWaitCar` | Color Wait Car | R/W | `string`
`string` | Config | :white_large_square: | color_waitcar, format: #RRGGBB | `"#FFFF00"` | 72 | | `cwe`
`cloudWsEnabled` | Cloud WS Enabled | R/W | `boolean`
`bool` | Config | :white_large_square: | cloud websocket enabled | `true` | 73 | | `cws`
`cloudWsStarted` | Cloud WS Started | R | `boolean`
`bool` | Status | :white_large_square: | cloud websocket started | `true` | 74 | | `cwsc`
`cloudWsConnected` | Cloud WS Connected | R | `boolean`
`bool` | Status | :white_large_square: | cloud websocket connected | `true` | 75 | | `cwsca`
`cloudWsConnectedAge` | Cloud WS Connected Age | R | `integer`
`optional` | Status | :white_large_square: | cloud websocket connected (age) | `46954034` | 76 | | `data`
- | | | `string`
- | | :white_large_square: | | `"{\"i\":120,\"url\":\"https://data.wattpilot.io/data?e=\"}"` | 77 | | `dbm`
- | | | `string`
- | | :white_large_square: | | `""` | 78 | | `dccu`
- | | | `boolean`
- | | :white_large_square: | | `false` | 79 | | `dco`
- | | | `boolean`
- | | :white_large_square: | | `true` | 80 | | `deltaa`
`deltaCurrent` | deltaCurrent | R | `float`
`float` | Other | :white_large_square: | | | 81 | | `deltap`
`deltaPower` | Delta Power | R | `float`
`float` | Status | :white_large_square: | | | 82 | | `dll`
- | | | `string`
- | | :white_large_square: | | `"https://data.wattpilot.io/export?e="` | 83 | | `dns`
`dnsServer` | DNS Server | R | `object`
`object` | Status | :white_large_square: | DNS server | `{"dns": "0.0.0.0"}` | 84 | | `dwo`
`chargingEnergyLimit` | Charging Energy Limit | R/W | `float`
`optional` | Config | :heavy_check_mark: | charging energy limit, measured in Wh, null means disabled, not the next trip energy | `null` | 85 | | `ecf`
`espCpuFreq` | ESP CPU Frequency | R | `object`
`object` | Constant | :white_large_square: | ESP CPU freq (source: XTAL=0, PLL=1, 8M=2, APLL=3) | `{"source": 1, "source_freq_mhz": 320, "div": 2, "freq_mhz": 160}` | 86 | | `eci`
`espChipInfo` | ESP Chip Info | R | `object`
`object` | Constant | :white_large_square: | ESP chip info (model: ESP32=1, ESP32S2=2, ESP32S3=4, ESP32C3=5; features: EMB_FLASH=bit0, WIFI_BGN=bit1, BLE=bit4, BT=bit5) | `{"model": 1, "features": 50, "cores": 2, "revision": 3}` | 87 | | `efh`
`espFreeHeap` | ESP Free Heap | R | `integer`
`size_t` | Status | :white_large_square: | ESP free heap | `125920` | 88 | | `efh32`
`espFreeHeap32` | ESP Free Heap 32 | R | `integer`
`size_t` | Status | :white_large_square: | ESP free heap 32bit aligned | `125920` | 89 | | `efh8`
`espFreeHeap8` | ESP Free Heap 8 | R | `integer`
`size_t` | Status | :white_large_square: | ESP free heap 8bit aligned | `86848` | 90 | | `efi`
`espFlashInfo` | ESP Flash Info | R | `object`
`object` | Constant | :white_large_square: | ESP Flash info (spi_mode: QIO=0, QOUT=1, DIO=2, DOUT=3, FAST_READ=4, SLOW_READ=5; spi_speed: 40M=0, 26M=1, 20M=2, 80M=15; spi_size: 1MB=0, 2MB=1, 4MB=2, 8MB=3, 16MB=4, MAX=5) | `null` | 91 | | `ehs`
`espHeapSize` | ESP Heap Size | R | `integer`
`size_t` | Status | :white_large_square: | ESP heap size | `282800` | 92 | | `emfh`
`espMinFreeHeap` | ESP Min Free Heap | R | `integer`
`size_t` | Status | :white_large_square: | ESP minimum free heap since boot | `78104` | 93 | | `emhb`
`espMaxHeap` | ESP Max Heap | R | `integer`
`size_t` | Status | :white_large_square: | ESP max size of allocated heap block since boot | `67572` | 94 | | `ens`
- | | | `string`
- | | :white_large_square: | | `""` | 95 | | `err`
`errorState` | Error State | R | `integer`
`optional` | Status | :heavy_check_mark: | error, null if internal error (None = 0, FiAc = 1, FiDc = 2, Phase = 3, Overvolt = 4, Overamp = 5, Diode = 6, PpInvalid = 7, GndInvalid = 8, ContactorStuck = 9, ContactorMiss = 10, FiUnknown = 11, Unknown = 12, Overtemp = 13, NoComm = 14, StatusLockStuckOpen = 15, StatusLockStuckLocked = 16, Reserved20 = 20, Reserved21 = 21, Reserved22 = 22, Reserved23 = 23, Reserved24 = 24) | `0` | 96 | | `esk`
`energySetKwh` | Energy Set kWh | R/W | `boolean`
`bool` | Config | :white_large_square: | energy set kwh (only stored for app) | `true` | 97 | | `esr`
`rtcResetReasons` | RTC Reset Reasons | R | `array`
`array` | Status | :white_large_square: | rtc_get_reset_reason for core 0 and 1 (NO_MEAN=0, POWERON_RESET=1, SW_RESET=3, OWDT_RESET=4, DEEPSLEEP_RESET=5, SDIO_RESET=6, TG0WDT_SYS_RESET=7, TG1WDT_SYS_RESET=8, RTCWDT_SYS_RESET=9, INTRUSION_RESET=10, TGWDT_CPU_RESET=11, SW_CPU_RESET=12, RTCWDT_CPU_RESET=13, EXT_CPU_RESET=14, RTCWDT_BROWN_OUT_RESET=15, RTCWDT_RTC_RESET=16) | `[12, 12]` | 98 | | `eto`
`energyCounterTotal` | Energy Counter Total | R | `integer`
`uint64` | Status | :heavy_check_mark: | energy_total, measured in Wh | `1076098` | 99 | | `etop`
`energyTotalPersisted` | Energy Total Persisted | R | `integer`
`uint64` | Status | :heavy_check_mark: | energy_total persisted, measured in Wh, without the extra magic to have live values | `1076098` | 100 | | `facwak`
`factoryWifiApKey` | Factory Wifi AP Key | R | `string`
`string` | Constant | :white_large_square: | WiFi AccessPoint Key RESET VALUE (factory) | `true` | 101 | | `fam`
`pvBatteryLimit` | PV Battery Limit | | `integer`
- | | :white_large_square: | Battery limit for PV surplus charging | `20` | 102 | | `fap`
`froniusAllowPause` | Fronius Allow Pause | R/W | `boolean`
`bool` | | :white_large_square: | Normally true. Set to false if your car disconnects when a charging pause occurs. Only VW ID.3/ID.4 seem to require this. | `false` | 103 | | `fbuf_age`
`fbufAge` | Fronius Age | | `integer`
- | | :white_large_square: | | `93639347` | 104 | | `fbuf_akkuMode`
`akkuMode` | Battery Mode | | `integer`
- | | :white_large_square: | | `1` | 105 | | `fbuf_akkuSOC`
`akkuSoc` | Battery SoC | | `float`
- | | :heavy_check_mark: | State of charge of the PV battery | `72.5` | 106 | | `fbuf_ohmpilotState`
`ohmpilotState` | Ohmpilot State | | `unknown`
- | | :white_large_square: | | `null` | 107 | | `fbuf_ohmpilotTemperature`
`ohmpilotTemperature` | Ohmpilot Temperature | | `unknown`
- | | :white_large_square: | | `null` | 108 | | `fbuf_pAcTotal`
`powerAcTotal` | Power AC Total | | `unknown`
- | | :white_large_square: | | `null` | 109 | | `fbuf_pAkku`
`powerAkku` | Power Akku | | `float`
- | | :heavy_check_mark: | Power that is consumed from the PV battery (or delivered into the battery, if negative) | `-3985.899` | 110 | | `fbuf_pGrid`
`powerGrid` | Power Grid | | `integer`
- | | :heavy_check_mark: | Power consumed from grid (or delivered to grid, if negative) | `11` | 111 | | `fbuf_pPv`
`powerPv` | Power PV | | `float`
- | | :heavy_check_mark: | PV power that is produced | `4701.407` | 112 | | `fcc`
- | | | `boolean`
- | | :white_large_square: | | `true` | 113 | | `fck`
- | | | `boolean`
- | | :white_large_square: | | `true` | 114 | | `fem`
`flashEncryptionMode` | Flash Encryption Mode | R | `integer`
`uint8` | Constant | :white_large_square: | Flash Encryption Mode (Disabled=0, Development=1, Release=2) | `2` | 115 | | `ferm`
`effectiveRoundingMode` | Effective Rounding Mode | R | `integer`
`uint8` | Status | :white_large_square: | effectiveRoundingMode | `1` | 116 | | `ffb`
`lockFeedback` | Lock Feedback | R | `integer`
`uint8` | Status | :heavy_check_mark: | lock feedback (NoProblem=0, ProblemLock=1, ProblemUnlock=2) | `0` | 117 | | `ffba`
`lockFeedbackAge` | Lock Feedback Age | R | `integer`
`optional` | Status | :white_large_square: | lock feedback (age) | `null` | 118 | | `ffna`
`factoryFriendlyName` | Factory Friendly Name | R | `string`
`string` | Constant | :white_large_square: | factoryFriendlyName | `"Wattpilot_"` | 119 | | `fhi`
- | | | `boolean`
- | | :white_large_square: | | `true` | 120 | | `fhz`
`frequency` | Frequency | R | `float`
`optional` | Status | :heavy_check_mark: | Power grid frequency (~50Hz) or 0 if unknown | `49.815` | 121 | | `fi23`
- | | | `boolean`
- | | :white_large_square: | | `true` | 122 | | `fio23`
- | | | `boolean`
- | | :white_large_square: | | `true` | 123 | | `fit`
- | | | `integer`
- | | :white_large_square: | | `1` | 124 | | `fml`
- | | | `string`
- | | :white_large_square: | | `"grid"` | 125 | | `fmmp`
- | | | `integer`
- | | :white_large_square: | | `0` | 126 | | `fmt`
`minChargeTime` | Min Charge Time | R/W | `integer`
`milliseconds` | Config | :white_large_square: | minChargeTime in milliseconds | `900000` | 127 | | `fna`
`friendlyName` | Friendly Name | R/W | `string`
`string` | Config | :heavy_check_mark: | friendlyName | `""` | 128 | | `fntp`
- | | | `unknown`
- | | :white_large_square: | | `null` | 129 | | `fot`
`ohmpilotTemperatureLimit` | Ohmpilot Temperature Limit | | `integer`
- | | :heavy_check_mark: | | `20` | 130 | | `frc`
`forceState` | Force State | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | forceState (Neutral=0, Off=1, On=2) | `0` | 131 | | `frci`
- | | | `boolean`
- | | :white_large_square: | | `true` | 132 | | `fre`
- | | | `boolean`
- | | :white_large_square: | | `false` | 133 | | `frm`
`roundingMode` | Rounding Mode | R | `integer`
`uint8` | Config | :white_large_square: | roundingMode PreferPowerFromGrid=0, Default=1, PreferPowerToGrid=2 | `1` | 134 | | `fsp`
`forceSinglePhase` | Force Single Phase | R/W | `boolean`
`bool` | Status | :white_large_square: | force_single_phase (shows if currently single phase charge is enforced) | `false` | 135 | | `fsptws`
`forceSinglePhaseToggleWishedSince` | Force Single Phase Toggle Wished Since | R | `integer`
`optional` | Status | :white_large_square: | force single phase toggle wished since | `28771782` | 136 | | `fst`
`startingPower` | Starting Power | R/W | `float`
`float` | Config | :heavy_check_mark: | startingPower in watts. This is the minimum power at which charging can be started. | `1400` | 137 | | `fte`
`froniusTripEnergy` | Fronius Trip Energy | R/W | `integer`
`int` | | :white_large_square: | Minimum energy in Watthours to charge in next trip mode. | `50000` | 138 | | `ftlf`
- | | | `boolean`
- | | :white_large_square: | | `false` | 139 | | `ftls`
- | | | `unknown`
- | | :white_large_square: | | `null` | 140 | | `ftt`
`froniusTripTime` | Fronius Trip Time | R/W | `integer`
`int` | | :white_large_square: | Starting time of your next trip in seconds from midnight local time. 3600 = 01:00 am local time | `25200` | 141 | | `ful`
`useDynamicPricing` | useDynamicPricing | | `boolean`
- | | :white_large_square: | Uses dynamic electricity pricing (Lumina, aWattar) | `false` | 142 | | `fup`
`usePvSurplus` | PV Surplus | R/W | `boolean`
`bool` | Config | :white_large_square: | Use PV surplus charging | `true` | 143 | | `fwan`
`factoryWifiApName` | Factory WiFi AP Name | R | `string`
`string` | Constant | :white_large_square: | factoryWifiApName | `"Wattpilot_"` | 144 | | `fwc`
`firmwareCarControl` | Firmware Car Control | R | `string`
`string` | Constant | :white_large_square: | firmware from CarControl | `10` | 145 | | `fwv`
`firmwareVersion` | Firmware Version | R | `string`
`string` | Constant | :heavy_check_mark: | Version of the Wattpilot firmware | `36.3` | 146 | | `fzf`
`zeroFeedin` | Zero Feedin | R/W | `boolean`
`bool` | Config | :white_large_square: | zeroFeedin | `false` | 147 | | `gme`
- | | | `boolean`
- | | :white_large_square: | | `false` | 148 | | `gmk`
- | | | `string`
- | | :white_large_square: | | `""` | 149 | | `host`
`hostname` | Hostname | R | `string`
`optional` | Status | :white_large_square: | hostname used on STA interface | `"Wattpilot_"` | 150 | | `hsa`
`httpStaAuthentication` | HTTP STA Authentication | R/W | `boolean`
`bool` | Config | :white_large_square: | httpStaAuthentication | `true` | 151 | | `hsta`
- | | | `string`
- | | :white_large_square: | | `"Wattpilot_"` | 152 | | `hsts`
- | | | `string`
- | | :white_large_square: | | `"Wattpilot_"` | 153 | | `hws`
`httpStaReachable` | HTTP STA Reachable | R/W | `boolean`
`bool` | Config | :white_large_square: | httpStaReachable, defines if the local webserver should be reachable from the customers WiFi | `true` | 154 | | `ido`
`inverterDataOverride` | Inverter Data Override | R | `object`
`optional` | Config | :white_large_square: | Inverter data override | `null` | 155 | | `imd`
- | | | `boolean`
- | | :white_large_square: | | `false` | 156 | | `imi`
- | | | `integer`
- | | :white_large_square: | | `0` | 157 | | `immr`
- | | | `integer`
- | | :white_large_square: | | `20` | 158 | | `imp`
- | | | `string`
- | | :white_large_square: | | `"_tcp"` | 159 | | `ims`
- | | | `string`
- | | :white_large_square: | | `"_Fronius-SE-Inverter"` | 160 | | `imse`
- | | | `boolean`
- | | :white_large_square: | | `true` | 161 | | `inva`
`inverterDataAge` | Inverter Data Age | R | `integer`
`milliseconds` | Status | :white_large_square: | age of inverter data | | 162 | | `irs`
- | | | `boolean`
- | | :white_large_square: | | `false` | 163 | | `isml`
- | | | `boolean`
- | | :white_large_square: | | `false` | 164 | | `iuse`
- | | | `boolean`
- | | :white_large_square: | | `true` | 165 | | `las`
- | | | `integer`
- | | :white_large_square: | | `0` | 166 | | `lbh`
- | | | `unknown`
- | | :white_large_square: | | `null` | 167 | | `lbp`
`lastButtonPress` | Last Button Press | R | `integer`
`milliseconds` | Status | :white_large_square: | lastButtonPress in milliseconds | `null` | 168 | | `lbr`
`ledBrightness` | LED Brightness | R/W | `integer`
`uint8` | Config | :white_large_square: | led_bright, 0-255 | `255` | 169 | | `lbs`
- | | | `integer`
- | | :white_large_square: | | `806` | 170 | | `lccfc`
`lastCarStateChangedFromCharging` | Last Car State Changed From Charging | R | `integer`
`optional` | Status | :white_large_square: | lastCarStateChangedFromCharging (in ms) | `7157569` | 171 | | `lccfi`
`lastCarStateChangedFromIdle` | Last Car State Changed From Idle | R | `integer`
`optional` | Status | :white_large_square: | lastCarStateChangedFromIdle (in ms) | `5369660` | 172 | | `lcctc`
`lastCarStateChangedToCharging` | Last Car State Changed To Charging | R | `integer`
`optional` | Status | :white_large_square: | lastCarStateChangedToCharging (in ms) | `5369660` | 173 | | `lch`
- | | | `integer`
- | | :white_large_square: | | `5369660` | 174 | | `lck`
`effectiveLockSetting` | Effective Lock Setting | R | `integer`
`uint8` | Status | :heavy_check_mark: | Effective lock setting, as sent to Charge Ctrl (Normal=0, AutoUnlock=1, AlwaysLock=2, ForceUnlock=3) | `0` | 175 | | `led`
`ledInfo` | LED Info | R | `object`
`object` | Status | :white_large_square: | internal infos about currently running led animation | `{"id": 5, "name": "Finished", "norwayOverlay": true, "modeOverlay": true, "subtype": "renderCmds", "ranges": [{"from": 0, "to": 31, "colors": ["#00FF00"]}]}` | 176 | | `ledo`
- | | | `unknown`
- | | :white_large_square: | | `null` | 177 | | `lfspt`
`lastForceSinglePhaseToggle` | Last Force Single Phase Toggle | R | `integer`
`optional` | Status | :white_large_square: | last force single phase toggle | `null` | 178 | | `llr`
- | | | `integer`
- | | :white_large_square: | | `2` | 179 | | `lmo`
`logicMode` | Logic Mode | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | logic mode (Default=3, Awattar=4, AutomaticStop=5) | `3` | 180 | | `lmsc`
`lastModelStatusChange` | Last Model Status Change | R | `integer`
`milliseconds` | Status | :white_large_square: | last model status change | `28822622` | 181 | | `loa`
`loadBalancingAmpere` | Load Balancing Current | R | `integer`
`optional` | Status | :white_large_square: | load balancing ampere | `null` | 182 | | `loc`
`localTime` | Local Time | R | `string`
`string` | Status | :white_large_square: | local time | `"2022-03-06T11:59:38.182.123 +01:00"` | 183 | | `loe`
`loadBalancingEnabled` | Load Balancing Enabled | R/W | `boolean`
`bool` | Config | :white_large_square: | Load balancing enabled | `false` | 184 | | `lof`
`loadFallback` | Load Fallback | R/W | `integer`
`uint8` | Config | :white_large_square: | load_fallback | `0` | 185 | | `log`
`loadGroupId` | Load Group ID | R/W | `string`
`string` | Config | :white_large_square: | load_group_id | `""` | 186 | | `loi`
- | | | `boolean`
- | | :white_large_square: | | `false` | 187 | | `lom`
`loadBalancingMembers` | Load Balancing Members | R | `array`
`array` | Status | :white_large_square: | load balancing members | `null` | 188 | | `lop`
`loadPriority` | Load Priority | R/W | `integer`
`uint16` | Config | :white_large_square: | load_priority | `50` | 189 | | `los`
`loadBalancingStatus` | Load Balancing Status | R | `string`
`optional` | Status | :white_large_square: | load balancing status | `null` | 190 | | `lot`
`loadBalancingTotalAmpere` | Load Balancing Current Total | R/W | `integer`
`uint32` | Config | :white_large_square: | load balancing total amp | `32` | 191 | | `loty`
`loadBalancingType` | Load Balancing Type | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | load balancing type (Static=0, Dynamic=1) | `0` | 192 | | `lps`
- | | | `integer`
- | | :white_large_square: | | `63` | 193 | | `lpsc`
`lastPvSurplusCalculation` | Last PV Surplus Calculation | R | `integer`
`milliseconds` | Status | :white_large_square: | last pv surplus calculation | `28771782` | 194 | | `lrc`
- | | | `unknown`
- | | :white_large_square: | | `null` | 195 | | `lri`
- | | | `unknown`
- | | :white_large_square: | | `null` | 196 | | `lrr`
- | | | `unknown`
- | | :white_large_square: | | `null` | 197 | | `lse`
`ledSaveEnergy` | LED Save Energy | R/W | `boolean`
`bool` | Config | :white_large_square: | led_save_energy | `false` | 198 | | `lssfc`
`lastStaSwitchedFromConnected` | Last STA Switched From Connected | R | `integer`
`optional` | Status | :white_large_square: | lastStaSwitchedFromConnected (in milliseconds) | `null` | 199 | | `lsstc`
`lastStaSwitchedToConnected` | Last STA Switched To Connected | R | `integer`
`optional` | Status | :white_large_square: | lastStaSwitchedToConnected (in milliseconds) | `7970` | 200 | | `maca`
- | | | `string`
- | | :white_large_square: | | `""` | 201 | | `macs`
- | | | `string`
- | | :white_large_square: | | `""` | 202 | | `map`
`loadMapping` | Load Mapping | R/W | `array`
`array` | Config | :white_large_square: | load_mapping (uint8_t[3]) | `[1, 2, 3]` | 203 | | `mca`
`minChargingCurrent` | Min Charging Current | R/W | `integer`
`uint8` | Config | :white_large_square: | minChargingCurrent | `6` | 204 | | `mci`
`minimumChargingInterval` | Minimum Charging Interval | R/W | `integer`
`milliseconds` | Config | :white_large_square: | minimumChargingInterval in milliseconds (0 means disabled) | `0` | 205 | | `mcpd`
`minChargePauseDuration` | Min Charge Pause Duration | R/W | `integer`
`milliseconds` | Config | :white_large_square: | minChargePauseDuration in milliseconds (0 means disabled) | `0` | 206 | | `mcpea`
`minChargePauseEndsAt` | Min Charge Pause End | R/W | `integer`
`optional` | Status | :white_large_square: | minChargePauseEndsAt (set to null to abort current minChargePauseDuration) | `null` | 207 | | `mod`
`moduleHwPcbVersion` | Module HW PCB Version | R | `integer`
`uint8` | Constant | :white_large_square: | Module hardware pcb version (0, 1, ...) | `1` | 208 | | `modelStatus`
`modelStatus` | Model Status | R | `integer`
`uint8` | Status | :heavy_check_mark: | Reason why we allow charging or not right now (NotChargingBecauseNoChargeCtrlData=0, NotChargingBecauseOvertemperature=1, NotChargingBecauseAccessControlWait=2, ChargingBecauseForceStateOn=3, NotChargingBecauseForceStateOff=4, NotChargingBecauseScheduler=5, NotChargingBecauseEnergyLimit=6, ChargingBecauseAwattarPriceLow=7, ChargingBecauseAutomaticStopTestLadung=8, ChargingBecauseAutomaticStopNotEnoughTime=9, ChargingBecauseAutomaticStop=10, ChargingBecauseAutomaticStopNoClock=11, ChargingBecausePvSurplus=12, ChargingBecauseFallbackGoEDefault=13, ChargingBecauseFallbackGoEScheduler=14, ChargingBecauseFallbackDefault=15, NotChargingBecauseFallbackGoEAwattar=16, NotChargingBecauseFallbackAwattar=17, NotChargingBecauseFallbackAutomaticStop=18, ChargingBecauseCarCompatibilityKeepAlive=19, ChargingBecauseChargePauseNotAllowed=20, NotChargingBecauseSimulateUnplugging=22, NotChargingBecausePhaseSwitch=23, NotChargingBecauseMinPauseDuration=24) | `15` | 209 | | `mptwt`
`minPhaseToggleWaitTime` | Min Phase Toggle Wait Time | R/W | `integer`
`milliseconds` | Config | :white_large_square: | min phase toggle wait time (in milliseconds) | `600000` | 210 | | `mpwst`
`minPhaseWishSwitchTime` | Min Phase Wish Switch Time | R/W | `integer`
`milliseconds` | Config | :white_large_square: | min phase wish switch time (in milliseconds) | `120000` | 211 | | `msca`
- | | | `integer`
- | | :white_large_square: | | `0` | 212 | | `mscs`
- | | | `integer`
- | | :white_large_square: | | `188` | 213 | | `msi`
`modelStatusInternal` | Model Status Internal | R | `integer`
`uint8` | Status | :white_large_square: | Reason why we allow charging or not right now INTERNAL without cpDisabledRequest | `15` | 214 | | `mws`
- | | | `boolean`
- | | :white_large_square: | | `true` | 215 | | `nif`
`defaultRoute` | Default Route | R | `string`
`string` | Status | :white_large_square: | Default route | `"st"` | 216 | | `nmo`
`norwayMode` | Norway Mode | R/W | `boolean`
`bool` | Config | :white_large_square: | norway_mode / ground check enabled when norway mode is disabled (inverted) | `false` | 217 | | `nrg`
`energy` | Charging Energy | R | `array`
`array` | Status | :heavy_check_mark: | energy array, U (L1, L2, L3, N), I (L1, L2, L3), P (L1, L2, L3, N, Total), pf (L1, L2, L3, N) | `[235, 234, 234, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]` | 218 | | `nvs`
- | | | `object`
- | | :white_large_square: | | `{"used_entries": 120, "free_entries": 7944, "total_entries": 8064, "namespace_count": 2, "nvs_handle_user": 52}` | 219 | | `obm`
- | | | `unknown`
- | | :white_large_square: | | `null` | 220 | | `oca`
`otaCloudApp` | OTA Cloud App | R | `object`
`optional` | Status | :white_large_square: | ota cloud app description | `null` | 221 | | `occa`
- | | | `integer`
- | | :white_large_square: | | `2` | 222 | | `ocl`
`otaCloudLength` | OTA Cloud Length | R | `integer`
`optional` | Status | :white_large_square: | ota from cloud length (total size) | `null` | 223 | | `ocm`
`otaCloudMessage` | OTA Cloud Message | R | `string`
`string` | Status | :white_large_square: | ota from cloud message | `""` | 224 | | `ocp`
`otaCloudProgress` | OTA Cloud Progress | R | `integer`
`int` | Status | :white_large_square: | ota from cloud progress | `0` | 225 | | `ocppc`
- | | | `boolean`
- | | :white_large_square: | | `false` | 226 | | `ocppca`
- | | | `unknown`
- | | :white_large_square: | | `null` | 227 | | `ocppe`
- | | | `boolean`
- | | :white_large_square: | | `false` | 228 | | `ocpph`
- | | | `integer`
- | | :white_large_square: | | `3600` | 229 | | `ocppi`
- | | | `integer`
- | | :white_large_square: | | `0` | 230 | | `ocppl`
- | | | `integer`
- | | :white_large_square: | | `1` | 231 | | `ocpps`
- | | | `boolean`
- | | :white_large_square: | | `false` | 232 | | `ocppu`
- | | | `string`
- | | :white_large_square: | | `"ws://echo.websocket.org/"` | 233 | | `ocs`
`otaCloudStatus` | OTA Cloud Status | R | `integer`
`uint8` | Status | :heavy_check_mark: | ota from cloud status (Idle=0, Updating=1, Failed=2, Succeeded=3) | `0` | 234 | | `ocu`
`otaCloudBranches` | OTA Cloud Branches | R | `array`
`array` | Status | :white_large_square: | list of available firmware branches | `["__default"]` | 235 | | `ocuca`
`otaCloudUseClientAuth` | OTA Cloud Use Client Auth | R | `boolean`
`bool` | Config | :white_large_square: | ota cloud use client auth (if keys were setup correctly) | `true` | 236 | | `oem`
`oemManufacturer` | OEM Manufacturer | R | `string`
`string` | Constant | :white_large_square: | OEM manufacturer | `"fronius"` | 237 | | `onv`
`otaNewestVersion` | OTA Newest Version | R | `string`
`string` | Status | :white_large_square: | OverTheAirUpdate newest version | `36.3` | 238 | | `otap`
`otaPartition` | OTA Partition | R | `object`
`optional` | Constant | :white_large_square: | currently used OTA partition | `{"type": 0, "subtype": 16, "address": 1441792, "size": 4194304, "label": "app_0", "encrypted": true}` | 239 | | `pakku`
`pAkku` | Power Akku | R | `float`
`optional` | Status | :white_large_square: | pAkku in W | | 240 | | `part`
`partitionTable` | Partition Table | R | `array`
`array` | Constant | :white_large_square: | partition table | `[{"type": 1, "subtype": 2, "address": 65536, "size": 262144, "label": "nvs", "encrypted": false}, {"type": 1, "subtype": 1, "address": 327680, "size": 4096, "label": "phy_init", "encrypted": false}]` | 241 | | `pgrid`
`pGrid` | Power Grid | R | `float`
`optional` | Status | :white_large_square: | pGrid in W | | 242 | | `pha`
`phases` | Phases | R | `array`
`optional` | Status | :white_large_square: | phases | `"[false, false, false, true, true, true]"` | 243 | | `pnp`
`numberOfPhases` | Number of Phases | R | `integer`
`uint8` | Status | :white_large_square: | numberOfPhases | `0` | 244 | | `po`
`prioOffset` | Prio Offset | R/W | `float`
`float` | Config | :white_large_square: | prioOffset in W | `-300` | 245 | | `ppv`
`pPv` | Power PV | R | `float`
`optional` | Status | :white_large_square: | pPv in W | | 246 | | `psh`
`phaseSwitchHysteresis` | Phase Switch Hysteresis | R/W | `float`
`float` | Config | :white_large_square: | phaseSwitchHysteresis in W | `500` | 247 | | `psm`
`phaseSwitchMode` | Phase Switch Mode | R/W | `integer`
`uint8` | Config | :white_large_square: | phaseSwitchMode (Auto=0, Force_1=1, Force_3=2) | `0` | 248 | | `psmd`
`forceSinglePhaseDuration` | Force Single Phase Duration | R/W | `integer`
`milliseconds` | Config | :white_large_square: | forceSinglePhaseDuration (in milliseconds) | `10000` | 249 | | `pto`
`partitionTableOffset` | Partition Table Offset | R | `integer`
`uint32` | Constant | :white_large_square: | partition table offset in flash | `61440` | 250 | | `pvopt_averagePAkku`
`averagePAkku` | Average Power Akku | R | `float`
`float` | Status | :heavy_check_mark: | averagePAkku | `-5213.455` | 251 | | `pvopt_averagePGrid`
`averagePGrid` | Average Power Grid | R | `float`
`float` | Status | :heavy_check_mark: | averagePGrid | `1.923335` | 252 | | `pvopt_averagePOhmpilot`
`avgPowerOhmpilot` | Average Power Ohmpilot | R | `integer`
- | | :heavy_check_mark: | | `0` | 253 | | `pvopt_averagePPv`
`averagePPv` | Average Power PV | R | `float`
`float` | Status | :heavy_check_mark: | averagePPv | `6008.117` | 254 | | `pvopt_deltaA`
`deltaCurrent` | Delta Current | R | `integer`
- | | :heavy_check_mark: | | `0` | 255 | | `pvopt_deltaP`
`deltaPower` | Delta Power | R | `float`
- | | :heavy_check_mark: | | `-1256.149` | 256 | | `pvopt_specialCase`
`pvOptSpecialCase` | PVOpt Special Case | R | `integer`
- | | :white_large_square: | | `0` | 257 | | `pwm`
`phaseWishMode` | Phase Wish Mode | R | `integer`
`uint8` | Status | :heavy_check_mark: | phase wish mode for debugging / only for pv optimizing / used for timers later (Force_3=0, Wish_1=1, Wish_3=2) | `0` | 258 | | `qsc`
`queueSizeCloud` | Queue Size Cloud | R | `integer`
`size_t` | Status | :white_large_square: | queue size cloud | `0` | 259 | | `qsw`
`queueSizeWs` | Queue Size WS | R | `integer`
`size_t` | Status | :white_large_square: | queue size webserver/websocket | `5` | 260 | | `rbc`
`rebootCounter` | Reboot Counter | R | `integer`
`uint32` | Status | :white_large_square: | Number of device reboots | `32` | 261 | | `rbt`
`timeSinceBoot` | Time Since Boot | R | `integer`
`milliseconds` | Status | :white_large_square: | time since boot in milliseconds | `93641458` | 262 | | `rcd`
`residualCurrentDetection` | Residual Current Detection | R | `integer`
`optional` | Status | :white_large_square: | residual current detection (in microseconds) WILL CHANGE IN FUTURE | `null` | 263 | | `rcsl`
- | | | `boolean`
- | | :white_large_square: | | `false` | 264 | | `rfb`
`relayFeedback` | Relay Feedback | R | `integer`
`int` | Status | :white_large_square: | Relay Feedback | `1699` | 265 | | `rfide`
- | | | `boolean`
- | | :white_large_square: | | `true` | 266 | | `rial`
- | | | `boolean`
- | | :white_large_square: | | `false` | 267 | | `riml`
- | | | `boolean`
- | | :white_large_square: | | `false` | 268 | | `risl`
- | | | `boolean`
- | | :white_large_square: | | `false` | 269 | | `riul`
- | | | `boolean`
- | | :white_large_square: | | `false` | 270 | | `rmdns`
- | | | `boolean`
- | | :white_large_square: | | `false` | 271 | | `rr`
`espResetReason` | ESP Reset Reason | R | `integer`
`uint8` | Status | :white_large_square: | esp_reset_reason (UNKNOWN=0, POWERON=1, EXT=2, SW=3, PANIC=4, INT_WDT=5, TASK_WDT=6, WDT=7, DEEPSLEEP=8, BROWNOUT=9, SDIO=10) | `4` | 272 | | `rrca`
- | | | `integer`
- | | :white_large_square: | | `2` | 273 | | `rssi`
`wifiRssi` | WIFI Signal Strength | R | `integer`
`optional` | Status | :heavy_check_mark: | RSSI signal strength | `-66` | 274 | | `rst`
`rebootCharger` | Reboot Charger | W | `any`
`any` | Other | :white_large_square: | Reboot charger | | 275 | | `sau`
- | | | `boolean`
- | | :white_large_square: | | `false` | 276 | | `sbe`
`secureBootEnabled` | Secure Boot Enabled | R | `boolean`
`bool` | Constant | :white_large_square: | Secure boot enabled | `true` | 277 | | `scaa`
`wifiScanAge` | WiFi Scan Age | R | `integer`
`milliseconds` | Status | :white_large_square: | wifi scan age | `6429` | 278 | | `scan`
`wifiScanResult` | Scanned WIFI Hotspots | R | `array`
`array` | Status | :white_large_square: | wifi scan result (encryptionType: OPEN=0, WEP=1, WPA_PSK=2, WPA2_PSK=3, WPA_WPA2_PSK=4, WPA2_ENTERPRISE=5, WPA3_PSK=6, WPA2_WPA3_PSK=7) | `[{"ssid": "", "encryptionType": 3, "rssi": -65, "channel": 6, "bssid": "", "f": [4, 4, true, true, true, false, false, false, false, "DE"]}, {"ssid": "", "encryptionType": 3, "rssi": -65, "channel": 6, "bssid": "", "f": [4, 4, true, true, true, false, false, false, false, "DE"]}]` | 279 | | `scas`
`wifiScanStatus` | WIFI Scan Status | R | `integer`
`uint8` | Status | :heavy_check_mark: | wifi scan status (None=0, Scanning=1, Finished=2, Failed=3) | `2` | 280 | | `sch_satur`
`schedulerSaturday` | Charging Schedule Saturday | R/W | `object`
`object` | Config | :white_large_square: | scheduler_saturday, control enum values: Disabled=0, Inside=1, Outside=2 | `{"control": 0, "ranges": [{"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}, {"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}]}` | 281 | | `sch_sund`
`schedulerSunday` | Charging Schedule Sunday | R/W | `object`
`object` | Config | :white_large_square: | scheduler_sunday, control enum values: Disabled=0, Inside=1, Outside=2 | `{"control": 0, "ranges": [{"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}, {"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}]}` | 282 | | `sch_week`
`schedulerWeekday` | Charging Schedule Weekday | R/W | `object`
`object` | Config | :white_large_square: | scheduler_weekday, control enum values: Disabled=0, Inside=1, Outside=2 | `{"control": 0, "ranges": [{"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}, {"begin": {"hour": 0, "minute": 0}, "end": {"hour": 0, "minute": 0}}]}` | 283 | | `sdca`
- | | | `integer`
- | | :white_large_square: | | `2` | 284 | | `sh`
`stopHysteresis` | Stop Hysteresis | R/W | `float`
`float` | Config | :white_large_square: | stopHysteresis in W | `200` | 285 | | `smca`
- | | | `integer`
- | | :white_large_square: | | `2` | 286 | | `smd`
- | | | `unknown`
- | | :white_large_square: | | `null` | 287 | | `spl3`
`threePhaseSwitchLevel` | Three Phase Switch Level | R/W | `float`
`float` | Config | :white_large_square: | threePhaseSwitchLevel | `4200` | 288 | | `sse`
`serialNumber` | Serial Number | R | `string`
`string` | Constant | :heavy_check_mark: | serial number | `""` | 289 | | `stao`
- | | | `unknown`
- | | :white_large_square: | | `null` | 290 | | `su`
`simulateUnplugging` | Simulate Unplugging | R/W | `boolean`
`bool` | Config | :white_large_square: | simulateUnplugging or simulateUnpluggingShort? (see v2) | `false` | 291 | | `sua`
`simulateUnpluggingAlways` | Simulate Unplugging Always | R/W | `boolean`
`bool` | Config | :white_large_square: | simulateUnpluggingAlways | `false` | 292 | | `sumd`
`simulateUnpluggingDuration` | Simulate Unplugging Duration | R/W | `integer`
`milliseconds` | Config | :white_large_square: | simulate unpluging duration (in milliseconds) | `5000` | 293 | | `swc`
- | | | `boolean`
- | | :white_large_square: | | `false` | 294 | | `tds`
`timezoneDaylightSavingMode` | Timezone Daylight Saving Mode | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | timezone daylight saving mode, None=0, EuropeanSummerTime=1, UsDaylightTime=2 | `1` | 295 | | `tma`
`temperatureSensors` | Temperature Sensors | R | `array`
`array` | Status | :heavy_check_mark: | temperature sensors | `[11, 16.75]` | 296 | | `tof`
`timezoneOffset` | Timezone Offset | R/W | `integer`
`minutes` | Config | :white_large_square: | timezone offset in minutes | `60` | 297 | | `tou`
- | | | `integer`
- | | :white_large_square: | | `0` | 298 | | `tpa`
`totalPowerAverage` | Total Power Average | R | `float`
`float` | Status | :white_large_square: | 30 seconds total power average (used to get better next-trip predictions) | `0` | 299 | | `tpck`
- | | | `array`
- | | :white_large_square: | | `["chargectrl", "i2c", "led", "wifi", "webserver", "mdns", "time", "cloud", "rfid", "temperature", "status", "froniusinverter", "button", "delta_http", "delta_cloud", "ota_cloud", "cmdhandler", "loadbalancing", "ocpp", "remotereq", "cloud_send"]` | 300 | | `tpcm`
- | | | `array`
- | | :white_large_square: | | `[4, 0, 3, 1, 0, 0, 0, 0, 43, 2, 53, 0, 0, 0, 50, 0, 0, 0, 0, 0, 10]` | 301 | | `trx`
`transaction` | Transaction | R/W | `integer`
`optional` | Status | :white_large_square: | transaction, null when no transaction, 0 when without card, otherwise 302 | cardIndex + 1 (1: 0. card, 2: 1. card, ...) 303 | | `null` | 304 | | `ts`
`timeServer` | Time Server | R | `string`
`string` | Config | :white_large_square: | time server | `"europe.pool.ntp.org"` | 305 | | `tse`
`timeServerEnabled` | Time Server Enabled | R/W | `boolean`
`bool` | Config | :heavy_check_mark: | time server enabled (NTP) | `false` | 306 | | `tsom`
`timeServerOperatingMode` | Time Server Operating Mode | R | `integer`
`uint8` | Status | :white_large_square: | time server operating mode (POLL=0, LISTENONLY=1) | `0` | 307 | | `tssi`
`timeServerSyncInterval` | Time Server Sync Interval | R | `integer`
`milliseconds` | Config | :white_large_square: | time server sync interval (in ms, 15s minimum) | `3600000` | 308 | | `tssm`
`timeServerSyncMode` | Time Server Sync Mode | R | `integer`
`uint8` | Config | :white_large_square: | time server sync mode (IMMED=0, SMOOTH=1) | `0` | 309 | | `tsss`
`timeServerSyncStatus` | Time Server Sync Status | R | `integer`
`uint8` | Config | :white_large_square: | time server sync status (RESET=0, COMPLETED=1, IN_PROGRESS=2) | `0` | 310 | | `typ`
`deviceType` | Device Type | R | `string`
`string` | Constant | :white_large_square: | Devicetype | `"wattpilot"` | 311 | | `uaca`
- | | | `integer`
- | | :white_large_square: | | `2` | 312 | | `upd`
- | | | `boolean`
- | | :white_large_square: | | `false` | 313 | | `upo`
`unlockPowerOutage` | Unlock Power Outage | R/W | `boolean`
`bool` | Config | :white_large_square: | unlock_power_outage | `false` | 314 | | `ust`
`cableLock` | Unlock Setting | R/W | `integer`
`uint8` | Config | :heavy_check_mark: | unlock_setting (Normal=0, AutoUnlock=1, AlwaysLock=2) | `0` | 315 | | `utc`
`utcTime` | UTC Time | R/W | `string`
`string` | Status | :white_large_square: | utc time | `"2022-03-06T10:59:38.181.250"` | 316 | | `var`
`variant` | Variant | R | `integer`
`uint8` | Constant | :heavy_check_mark: | variant: max Ampere value of unit (11: 11kW/16A, 22: 22kW/32A) | `11` | 317 | | `waap`
- | | | `integer`
- | | :white_large_square: | | `3` | 318 | | `wae`
- | | | `boolean`
- | | :white_large_square: | | `true` | 319 | | `wak`
`wifiApKey` | WiFi AP Key | R/W | `string`
`string` | Config | :white_large_square: | WiFi AccessPoint Key (read/write from http) | `false` | 320 | | `wan`
`wifiApName` | WiFi AP Name | R/W | `string`
`string` | Config | :white_large_square: | wifiApName | `"Wattpilot_"` | 321 | | `wapc`
- | | | `integer`
- | | :white_large_square: | | `1` | 322 | | `wcb`
`wifiCurrentMac` | WiFi Current MAC Address | R | `string`
`string` | Status | :white_large_square: | WiFi current mac address | `""` | 323 | | `wcch`
`httpConnectedClients` | HTTP Connected Clients | R | `integer`
`uint8` | Status | :white_large_square: | webserver connected clients as HTTP | `0` | 324 | | `wccw`
`wsConnectedClients` | WS Connected Clients | R | `integer`
`uint8` | Status | :white_large_square: | webserver connected clients as WEBSOCKET | `2` | 325 | | `wda`
- | | | `boolean`
- | | :white_large_square: | | `false` | 326 | | `wen`
`wifiEnabled` | WiFi Enabled | R/W | `boolean`
`bool` | Config | :white_large_square: | wifiEnabled (bool), turns off/on the WiFi (not the AccessPoint) | `true` | 327 | | `wfb`
`wifiFailedMac` | WiFi Failed MAC Address | R | `array`
`array` | Status | :white_large_square: | WiFi failed mac addresses | `null` | 328 | | `wh`
`energyCounterSinceStart` | Energy Counter Since Start | R | `float`
`double` | Status | :heavy_check_mark: | energy in Wh since car connected | `2133.804` | 329 | | `wifis`
`wifiConfigs` | WiFi Configs | R/W | `array`
`array` | Config | :white_large_square: | wifi configurations with ssids and keys, if you only want to change the second entry, send an array with 1 empty and 1 filled wifi config object: `[{}, {"ssid":"","key":""}]` | `[{"ssid": "", "key": true, "useStaticIp": false, "staticIp": "0.0.0.0", "staticSubnet": "0.0.0.0", "staticGateway": "0.0.0.0", "useStaticDns": false, "staticDns0": "0.0.0.0", "staticDns1": "0.0.0.0", "staticDns2": "0.0.0.0"}, {"ssid": "", "key": false, "useStaticIp": false, "staticIp": "0.0.0.0", "staticSubnet": "0.0.0.0", "staticGateway": "0.0.0.0", "useStaticDns": false, "staticDns0": "0.0.0.0", "staticDns1": "0.0.0.0", "staticDns2": "0.0.0.0"}]` | 330 | | `wpb`
`wifiPlannedMac` | WiFi Planned MAC | R | `array`
`array` | Status | :white_large_square: | WiFi planned mac addresses | `""` | 331 | | `wsc`
`wifiStaErrorCount` | WiFi STA Error Count | R | `integer`
`uint8` | Status | :white_large_square: | WiFi STA error count | `0` | 332 | | `wsm`
`wifiStaErrorMessage` | Wifi STA Error Message | R | `string`
`string` | Status | :white_large_square: | WiFi STA error message | `""` | 333 | | `wsmr`
- | | | `integer`
- | | :white_large_square: | | `-90` | 334 | | `wsms`
`wifiStateMachineState` | WIFI State Machine State | R | `integer`
`uint8` | Status | :heavy_check_mark: | WiFi state machine state (None=0, Scanning=1, Connecting=2, Connected=3) | `3` | 335 | | `wss`
`wifiSsid` | WIFI SSID | | `unknown`
- | | :white_large_square: | | | 336 | | `wst`
`wifiStaStatus` | WIFI STA Status | R | `integer`
`uint8` | Status | :heavy_check_mark: | WiFi STA status (IDLE_STATUS=0, NO_SSID_AVAIL=1, SCAN_COMPLETED=2, CONNECTED=3, CONNECT_FAILED=4, CONNECTION_LOST=5, DISCONNECTED=6, CONNECTING=8, DISCONNECTING=9, NO_SHIELD=10 (for compatibility with WiFi Shield library)) | `3` | 337 | | `zfo`
`zeroFeedinOffset` | Zero Feedin Offset | R/W | `float`
`float` | Config | :white_large_square: | zeroFeedinOffset in W | `200` | 338 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseimage=python:3-slim 2 | FROM ${baseimage} 3 | 4 | WORKDIR /src 5 | 6 | COPY . /src 7 | COPY ./entrypoint.sh / 8 | 9 | RUN pip install . 10 | 11 | ENTRYPOINT [ "/entrypoint.sh" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 joscha82 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Manifest.in: -------------------------------------------------------------------------------- 1 | include wattpilot.yaml 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wattpilot 2 | 3 | > :warning: This project is still in early development and might never leave this state 4 | 5 | `wattpilot` is a Python 3 (>= 3.10) module to interact with Fronius Wattpilot wallboxes which do not support (at the time of writting) a documented API. This functionality of this module utilized a undocumented websockets API, which is also utilized by the official Wattpilot.Solar mobile app. 6 | 7 | ## Wattpilot API Documentation 8 | 9 | See [API.md](API.md) for the current state of the API documentation this implementation is based on. 10 | 11 | It has been compiled from different sources, but primarily from: 12 | 13 | * [go-eCharger-API-v1](https://github.com/goecharger/go-eCharger-API-v1/blob/master/go-eCharger%20API%20v1%20EN.md) 14 | * [go-eCharger-API-v2](https://github.com/goecharger/go-eCharger-API-v2/blob/main/apikeys-en.md) 15 | 16 | ## Wattpilot Shell 17 | 18 | The shell provides an easy way to explore the available properties and get or set their values. 19 | 20 | ```bash 21 | # Install the wattpilot module, if not yet done so: 22 | pip install . 23 | ``` 24 | 25 | Run the interactive shell 26 | 27 | ```bash 28 | # Usage: 29 | export WATTPILOT_HOST= 30 | export WATTPILOT_PASSWORD= 31 | wattpilotshell 32 | Welcome to the Wattpilot Shell 0.2. Type help or ? to list commands. 33 | 34 | wattpilot> help 35 | 36 | Documented commands (use 'help -v' for verbose/'help ' for details): 37 | =========================================================================== 38 | alias docs ha macro propset run_script shortcuts 39 | config edit help mqtt quit server unwatch 40 | connect EOF history properties rawvalues set values 41 | disconnect exit info propget run_pyscript shell watch 42 | ``` 43 | 44 | The shell supports TAB-completion for all commands and their arguments. 45 | Detailed documentation can be found in [ShellCommands.md](ShellCommands.md). 46 | 47 | It's also possible to pass a single command to the shell to integrate it into scripts: 48 | 49 | ```bash 50 | # Usage: 51 | wattpilotshell " " 52 | 53 | # Examples: 54 | wattpilotshell "propget amp" 55 | wattpilotshell "propset amp 6" 56 | ``` 57 | 58 | ## MQTT Bridge Support 59 | 60 | It is possible to publish JSON messages received from Wattpilot and/or individual property value changes to an MQTT server. 61 | The easiest way to start the shell with MQTT support is using these environment variables: 62 | 63 | ```bash 64 | export MQTT_ENABLED=true 65 | export MQTT_HOST= 66 | export WATTPILOT_HOST= 67 | export WATTPILOT_PASSWORD= 68 | wattpilotshell 69 | ``` 70 | 71 | Pay attention to environment variables starting with `MQTT_` to fine-tune the MQTT support (e.g. which messages or properties should published to MQTT topics). 72 | 73 | MQTT support can be easily tested using mosquitto: 74 | 75 | ```bash 76 | # Start mosquitto in a separate console: 77 | mosquitto 78 | 79 | # Subscribe to topics in a separate console: 80 | mosquitto_sub -t 'wattpilot/#' -v 81 | ``` 82 | 83 | ## Home Assistant MQTT Discovery Support 84 | 85 | To enable Home Assistant integration (using MQTT) set `MQTT_ENABLED` and `HA_ENABLED` to `true` and make sure to correctly configure the [MQTT Integration](https://www.home-assistant.io/integrations/mqtt). 86 | It provides auto-discovery of entities using property configuration from [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml). 87 | The is the simplest possible way to start the shell with HA support: 88 | 89 | ```bash 90 | export MQTT_ENABLED=true 91 | export HA_ENABLED=true 92 | export MQTT_HOST= 93 | export WATTPILOT_HOST= 94 | export WATTPILOT_PASSWORD= 95 | wattpilotshell 96 | ``` 97 | 98 | Pay attention to environment variables starting with `HA_` to fine-tune the Home Assistant integration (e.g. which properties should be exposed). 99 | 100 | The discovery config published to MQTT can be tested using this in addition to the testing steps from MQTT above: 101 | 102 | MQTT support can be easily tested using mosquitto: 103 | 104 | ```bash 105 | # Subscribe to homeassisant topics in a separate console: 106 | mosquitto_sub -t 'homeassistant/#' -v 107 | ``` 108 | 109 | ## Docker Support 110 | 111 | The Docker images for the Wattpilot MQTT bridge with Home Assistant MQTT discovery can be found on [GitHub Packages](https://github.com/joscha82/wattpilot/pkgs/container/wattpilot): 112 | 113 | ```bash 114 | # Pull Image: 115 | docker pull ghcr.io/joscha82/wattpilot:latest 116 | # NOTE: Use the tag 'latest' for the latest release, a specific release version or 'main' for the current image of the not yet released main branch. 117 | 118 | # Create .env file with environment variables: 119 | cat .env 120 | HA_ENABLED=true 121 | MQTT_ENABLED=true 122 | MQTT_HOST= 123 | WATTPILOT_HOST= 124 | WATTPILOT_PASSWORD= 125 | 126 | # Run container (recommended with MQTT_ENABLED=true and HA_ENABLED=true - e.g. on a Raspberry Pi): 127 | docker-compose up -d 128 | ``` 129 | 130 | To diagnose the hundreds of Wattpilot parameters the shell can be started this way (typically recommended with `MQTT_ENABLED=false` and `HA_ENABLED=false` on a local machine, in case a Docker container with MQTT support may be running permanently on e.g. a Raspberry Pi): 131 | 132 | ```bash 133 | # Create .env file with environment variables: 134 | cat .env 135 | HA_ENABLED=false 136 | MQTT_ENABLED=false 137 | MQTT_HOST= 138 | WATTPILOT_HOST= 139 | WATTPILOT_PASSWORD= 140 | 141 | # Run the shell: 142 | docker-compose run wattpilot shell 143 | ``` 144 | 145 | ## Environment Variables 146 | 147 | For a complete list of supported environment variables see [ShellEnvVariables.md](ShellEnvVariables.md). 148 | 149 | ## HELP improving API definition in wattpilot.yaml 150 | 151 | The MQTT and Home Assistant support heavily depends on the API definition in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml) which has been compiled from different sources and does not yet contain a full set of information for all relevant properties. 152 | See [API.md](API.md) for a generated documentation of the available data. 153 | 154 | If you want to help, please have a look at the properties defined in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml) and fill in the missing pieces (e.g. `title`, `description`, `rw`, `jsonType`, `childProps`, `homeAssistant`, `device_class`, `unit_of_measurement`, `enabled_by_default`) to properties you care about. 155 | The file contains enough documentation and a lot of working examples to get you started. 156 | -------------------------------------------------------------------------------- /ShellCommands.md: -------------------------------------------------------------------------------- 1 | # Wattpilot Shell Commands 2 | 3 | ## alias 4 | 5 | ```bash 6 | Usage: alias [-h] SUBCOMMAND ... 7 | 8 | Manage aliases 9 | 10 | An alias is a command that enables replacement of a word by another string. 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | 15 | subcommands: 16 | SUBCOMMAND 17 | create create or overwrite an alias 18 | delete delete aliases 19 | list list aliases 20 | 21 | See also: 22 | macro 23 | 24 | ``` 25 | 26 | ## config 27 | 28 | ```bash 29 | Show configuration values 30 | Usage: config 31 | ``` 32 | 33 | ## connect 34 | 35 | ```bash 36 | Connect to Wattpilot 37 | Usage: connect 38 | ``` 39 | 40 | ## disconnect 41 | 42 | ```bash 43 | Disconnect from Wattpilot 44 | Usage: disconnect 45 | ``` 46 | 47 | ## docs 48 | 49 | ```bash 50 | Show markdown documentation for environment variables 51 | Usage: docs 52 | ``` 53 | 54 | ## edit 55 | 56 | ```bash 57 | Usage: edit [-h] [file_path] 58 | 59 | Run a text editor and optionally open a file with it 60 | 61 | The editor used is determined by a settable parameter. To set it: 62 | 63 | set editor (program-name) 64 | 65 | positional arguments: 66 | file_path optional path to a file to open in editor 67 | 68 | optional arguments: 69 | -h, --help show this help message and exit 70 | 71 | ``` 72 | 73 | ## exit 74 | 75 | ```bash 76 | Exit the shell 77 | Usage: exit 78 | ``` 79 | 80 | ## ha 81 | 82 | ```bash 83 | Control Home Assistant discovery (+MQTT client) 84 | Usage: ha [args...] 85 | 86 | Home Assistant commands: 87 | enable 88 | Enable a discovered entity representing the property 89 | NOTE: Re-enabling of disabled entities may still be broken in HA and require a restart of HA. 90 | disable 91 | Disable a discovered entity representing the property 92 | discover 93 | Let HA discover an entity representing the property 94 | properties 95 | List properties activated for HA discovery 96 | start 97 | Start HA MQTT discovery (using HA_* env variables) 98 | status 99 | Status of HA MQTT discovery 100 | stop 101 | Stop HA MQTT discovery 102 | undiscover 103 | Let HA remove a discovered entity representing the property 104 | NOTE: Removing of disabled entities may still be broken in HA and require a restart of HA. 105 | ``` 106 | 107 | ## help 108 | 109 | ```bash 110 | Usage: help [-h] [-v] [command] ... 111 | 112 | List available commands or provide detailed help for a specific command 113 | 114 | positional arguments: 115 | command command to retrieve help for 116 | subcommands subcommand(s) to retrieve help for 117 | 118 | optional arguments: 119 | -h, --help show this help message and exit 120 | -v, --verbose print a list of all commands with descriptions of each 121 | 122 | ``` 123 | 124 | ## history 125 | 126 | ```bash 127 | Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT_FILE | -c] [-s] [-x] 128 | [-v] [-a] 129 | [arg] 130 | 131 | View, run, edit, save, or clear previously entered commands 132 | 133 | positional arguments: 134 | arg empty all history items 135 | a one history item by number 136 | a..b, a:b, a:, ..b items by indices (inclusive) 137 | string items containing string 138 | /regex/ items matching regular expression 139 | 140 | optional arguments: 141 | -h, --help show this help message and exit 142 | -r, --run run selected history items 143 | -e, --edit edit and then run selected history items 144 | -o, --output_file FILE 145 | output commands to a script file, implies -s 146 | -t, --transcript TRANSCRIPT_FILE 147 | output commands and results to a transcript file, 148 | implies -s 149 | -c, --clear clear all history 150 | 151 | formatting: 152 | -s, --script output commands in script format, i.e. without command 153 | numbers 154 | -x, --expanded output fully parsed commands with any aliases and 155 | macros expanded, instead of typed commands 156 | -v, --verbose display history and include expanded commands if they 157 | differ from the typed command 158 | -a, --all display all commands, including ones persisted from 159 | previous sessions 160 | 161 | ``` 162 | 163 | ## info 164 | 165 | ```bash 166 | Print device infos 167 | Usage: info 168 | ``` 169 | 170 | ## macro 171 | 172 | ```bash 173 | Usage: macro [-h] SUBCOMMAND ... 174 | 175 | Manage macros 176 | 177 | A macro is similar to an alias, but it can contain argument placeholders. 178 | 179 | optional arguments: 180 | -h, --help show this help message and exit 181 | 182 | subcommands: 183 | SUBCOMMAND 184 | create create or overwrite a macro 185 | delete delete macros 186 | list list macros 187 | 188 | See also: 189 | alias 190 | 191 | ``` 192 | 193 | ## mqtt 194 | 195 | ```bash 196 | Control the MQTT bridge 197 | Usage: mqtt [args...] 198 | 199 | MQTT commands: 200 | properties 201 | List properties activated for MQTT publishing 202 | publish 203 | Enable publishing of messages or properties 204 | publish 205 | Enable publishing of a certain message type 206 | publish 207 | Enable publishing of a certain property 208 | start 209 | Start the MQTT bridge (using MQTT_* env variables) 210 | status 211 | Status of the MQTT bridge 212 | stop 213 | Stop the MQTT bridge 214 | unpublish 215 | Disable publishing of messages or properties 216 | unpublish 217 | Disable publishing of a certain message type 218 | unpublish 219 | Disable publishing of a certain property 220 | ``` 221 | 222 | ## properties 223 | 224 | ```bash 225 | List property definitions and values 226 | Usage: properties [propRegex] 227 | ``` 228 | 229 | ## propget 230 | 231 | ```bash 232 | Get a property value 233 | Usage: propget 234 | ``` 235 | 236 | ## propset 237 | 238 | ```bash 239 | Set a property value 240 | Usage: propset 241 | ``` 242 | 243 | ## quit 244 | 245 | ```bash 246 | Usage: quit [-h] 247 | 248 | Exit this application 249 | 250 | optional arguments: 251 | -h, --help show this help message and exit 252 | 253 | ``` 254 | 255 | ## rawvalues 256 | 257 | ```bash 258 | List raw values of properties (without value mapping) 259 | Usage: rawvalues [propRegex] [valueRegex] 260 | ``` 261 | 262 | ## run_pyscript 263 | 264 | ```bash 265 | Usage: run_pyscript [-h] script_path ... 266 | 267 | Run a Python script file inside the console 268 | 269 | positional arguments: 270 | script_path path to the script file 271 | script_arguments arguments to pass to script 272 | 273 | optional arguments: 274 | -h, --help show this help message and exit 275 | 276 | ``` 277 | 278 | ## run_script 279 | 280 | ```bash 281 | Usage: run_script [-h] [-t TRANSCRIPT_FILE] script_path 282 | 283 | Run commands in script file that is encoded as either ASCII or UTF-8 text 284 | 285 | Script should contain one command per line, just like the command would be 286 | typed in the console. 287 | 288 | If the -t/--transcript flag is used, this command instead records 289 | the output of the script commands to a transcript for testing purposes. 290 | 291 | positional arguments: 292 | script_path path to the script file 293 | 294 | optional arguments: 295 | -h, --help show this help message and exit 296 | -t, --transcript TRANSCRIPT_FILE 297 | record the output of the script as a transcript file 298 | 299 | ``` 300 | 301 | ## server 302 | 303 | ```bash 304 | Start in server mode (infinite wait loop) 305 | Usage: server 306 | ``` 307 | 308 | ## set 309 | 310 | ```bash 311 | Usage: set [-h] [param] [value] 312 | 313 | Set a settable parameter or show current settings of parameters 314 | 315 | positional arguments: 316 | param parameter to set or view 317 | value new value for settable 318 | 319 | optional arguments: 320 | -h, --help show this help message and exit 321 | 322 | ``` 323 | 324 | ## shell 325 | 326 | ```bash 327 | Usage: shell [-h] command ... 328 | 329 | Execute a command as if at the OS prompt 330 | 331 | positional arguments: 332 | command the command to run 333 | command_args arguments to pass to command 334 | 335 | optional arguments: 336 | -h, --help show this help message and exit 337 | 338 | ``` 339 | 340 | ## shortcuts 341 | 342 | ```bash 343 | Usage: shortcuts [-h] 344 | 345 | List available shortcuts 346 | 347 | optional arguments: 348 | -h, --help show this help message and exit 349 | 350 | ``` 351 | 352 | ## unwatch 353 | 354 | ```bash 355 | Unwatch a message or property 356 | Usage: unwatch 357 | ``` 358 | 359 | ## UpdateInverter 360 | 361 | ```bash 362 | Performs an Inverter Operation 363 | Usage: updateInverter pair|unpair 364 | is normally in the form 123.456789 365 | ``` 366 | 367 | ## values 368 | 369 | ```bash 370 | List values of properties (with value mapping enabled) 371 | Usage: values [propRegex] [valueRegex] 372 | ``` 373 | 374 | ## watch 375 | 376 | ```bash 377 | Watch an event, a message or a property 378 | Usage: watch 379 | ``` 380 | -------------------------------------------------------------------------------- /ShellEnvVariables.md: -------------------------------------------------------------------------------- 1 | # Wattpilot Shell Environment Variables 2 | 3 | |Environment Variable|Type|Default Value|Description| 4 | |--------------------|----|-------------|-----------| 5 | |`HA_DISABLED_ENTITIES`|`boolean`|`false`|Create disabled entities in Home Assistant| 6 | |`HA_ENABLED`|`boolean`|`false`|Enable Home Assistant Discovery| 7 | |`HA_PROPERTIES`|`list`||List of space-separated properties that should be discovered by Home Assistant (leave unset for all properties having `homeAssistant` set in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml)| 8 | |`HA_TOPIC_CONFIG`|`string`|`homeassistant/{component}/{uniqueId}/config`|Topic pattern for HA discovery config| 9 | |`HA_WAIT_INIT_S`|`integer`|`0`|Wait initial number of seconds after starting discovery (in addition to wait time depending on the number of properties). May be increased, if entities in HA are not populated with values.| 10 | |`HA_WAIT_PROPS_MS`|`integer`|`0`|Wait milliseconds per property after discovery before publishing property values. May be increased, if entities in HA are not populated with values.| 11 | |`MQTT_AVAILABLE_PAYLOAD`|`string`|`online`|Payload for the availability topic in case the MQTT bridge is online| 12 | |`MQTT_CLIENT_ID`|`string`|`wattpilot2mqtt`|MQTT client ID| 13 | |`MQTT_ENABLED`|`boolean`|`false`|Enable MQTT| 14 | |`MQTT_HOST`|`string`||MQTT host to connect to| 15 | |`MQTT_MESSAGES`|`list`||List of space-separated message types to be published to MQTT (leave unset for all messages)| 16 | |`MQTT_NOT_AVAILABLE_PAYLOAD`|`string`|`offline`|Payload for the availability topic in case the MQTT bridge is offline (last will message)| 17 | |`MQTT_PASSWORD`|`password`||Password for connecting to MQTT| 18 | |`MQTT_PORT`|`integer`|`1883`|Port of the MQTT host to connect to| 19 | |`MQTT_PROPERTIES`|`list`||List of space-separated property names to publish changes for (leave unset for all properties)| 20 | |`MQTT_PUBLISH_MESSAGES`|`boolean`|`false`|Publish received Wattpilot messages to MQTT| 21 | |`MQTT_PUBLISH_PROPERTIES`|`boolean`|`true`|Publish received property values to MQTT| 22 | |`MQTT_TOPIC_AVAILABLE`|`string`|`{baseTopic}/available`|Topic pattern to publish Wattpilot availability status to| 23 | |`MQTT_TOPIC_BASE`|`string`|`wattpilot`|Base topic for MQTT| 24 | |`MQTT_TOPIC_MESSAGES`|`string`|`{baseTopic}/messages/{messageType}`|Topic pattern to publish Wattpilot messages to| 25 | |`MQTT_TOPIC_PROPERTY_BASE`|`string`|`{baseTopic}/properties/{propName}`|Base topic for properties| 26 | |`MQTT_TOPIC_PROPERTY_SET`|`string`|`~/set`|Topic pattern to listen for property value changes for| 27 | |`MQTT_TOPIC_PROPERTY_STATE`|`string`|`~/state`|Topic pattern to publish property values to| 28 | |`MQTT_USERNAME`|`string`||Username for connecting to MQTT| 29 | |`WATTPILOT_AUTOCONNECT`|`boolean`|`true`|Automatically connect to Wattpilot on startup| 30 | |`WATTPILOT_AUTO_RECONNECT`|`boolean`|`true`|Automatically re-connect to Wattpilot on lost connections| 31 | |`WATTPILOT_CONNECT_TIMEOUT`|`integer`|`30`|Connect timeout for Wattpilot connection| 32 | |`WATTPILOT_HOST`|`string`||IP address of the Wattpilot device to connect to| 33 | |`WATTPILOT_INIT_TIMEOUT`|`integer`|`30`|Wait timeout for property initialization| 34 | |`WATTPILOT_LOGLEVEL`|`string`|`INFO`|Log level (CRITICAL,ERROR,WARNING,INFO,DEBUG)| 35 | |`WATTPILOT_PASSWORD`|`password`||Password for connecting to the Wattpilot device| 36 | |`WATTPILOT_RECONNECT_INTERVAL`|`integer`|`30`|Waiting time in seconds before a lost connection is re-connected| 37 | |`WATTPILOT_SPLIT_PROPERTIES`|`boolean`|`true`|Whether compound properties (e.g. JSON arrays or objects) should be decomposed into separate properties| 38 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Version 0.2 (2022-05-26) 2 | - New Featuer: mqtt Bridge (@ahochsteger) 3 | - Bugfixes 4 | 5 | Version 0.1 (2022-04-20) 6 | - New Featuer: shell (@ahochsteger) 7 | - New Feature: mqtt Bridge in shell (@ahochsteger) 8 | 9 | Version 0.0.1 (2022-02-17) 10 | - Initial testing release -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | wattpilot: 5 | image: ghcr.io/joscha82/wattpilot:main 6 | command: server 7 | restart: always 8 | environment: 9 | - HA_ENABLED=${HA_ENABLED:-true} 10 | - MQTT_ENABLED=${MQTT_ENABLED:-true} 11 | - MQTT_HOST=${MQTT_HOST:-} 12 | - MQTT_PORT=${MQTT_PORT:-1883} 13 | - MQTT_PUBLISH_MESSAGES=${MQTT_PUBLISH_MESSAGES:-false} 14 | - MQTT_PUBLISH_PROPERTIES=${MQTT_PUBLISH_PROPERTIES:-true} 15 | - WATTPILOT_LOGLEVEL=${WATTPILOT_LOGLEVEL:-INFO} 16 | - WATTPILOT_HOST=${WATTPILOT_HOST:-} 17 | - WATTPILOT_PASSWORD=${WATTPILOT_PASSWORD:-} 18 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cmd="${1}" 4 | shift 1 5 | 6 | case "${cmd}" in 7 | shell) 8 | wattpilotshell "${@}" 9 | ;; 10 | server) 11 | wattpilotshell server 12 | ;; 13 | esac 14 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import wattpilot 2 | from time import sleep 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("ip", help = "IP of Wattpilot Device") 7 | parser.add_argument("password", help = "Password of Wattpilot") 8 | 9 | 10 | args = parser.parse_args() 11 | 12 | ip = args.ip 13 | password = args.password 14 | 15 | 16 | solarwatt = wattpilot.Wattpilot(ip,password) 17 | solarwatt.connect() 18 | 19 | c=0 20 | while not solarwatt.connected and c<10: 21 | sleep(1) 22 | c=c+1 23 | 24 | ##Print Status 25 | print(solarwatt) 26 | 27 | ##Update arbitary property 28 | #solarwatt.send_update("cae",False) 29 | 30 | ## Set output power 31 | #solarwatt.set_power(16) 32 | 33 | ## Set Mode to Eco 34 | #solarwatt.set_mode(wattpilot.LoadMode.ECO) 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /gen-apidocs.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | 4 | WATTPILOT_API_FILE = "src/wattpilot/ressources/wattpilot.yaml" 5 | 6 | # Property code formatting: 7 | def pc(prop,key,alt=''): 8 | return f"`{prop[key]}`" if key in prop and prop[key] != "" else alt 9 | 10 | # Property key/alias formatting: 11 | def pk(prop): 12 | return pc(prop,'key','-') + "
" + pc(prop,'alias','-') 13 | 14 | # Property type formatting: 15 | def pt(prop): 16 | return pc(prop,'jsonType','-') + "
" + pc(prop,'type','-') 17 | 18 | # Property value formatting: 19 | def pv(prop,key,alt=''): 20 | return prop[key] if key in prop else alt 21 | 22 | # Property Home Assistant info formatting: 23 | def ha(prop): 24 | return ':heavy_check_mark:' if 'homeAssistant' in prop else ':white_large_square:' 25 | 26 | # Message and property example formatting: 27 | def e(d): 28 | return f"`{json.dumps(d['example'])}`" if "example" in d else "" 29 | 30 | with open(WATTPILOT_API_FILE, 'r') as stream: 31 | wpapidef = yaml.safe_load(stream) 32 | 33 | s = "# Wattpilot API Description\n\n" 34 | 35 | s += "## WebSocket Message Types\n\n" 36 | s += "| Key | Title | Description | Example |\n" 37 | s += "|-----|-------|-------------|---------|\n" 38 | for msg in wpapidef["messages"]: 39 | s += f"| `{msg['key']}` | {msg['title']} | {msg['description']} | {e(msg)} |\n" 40 | s += "\n" 41 | 42 | s += "## WebSocket API Properties\n\n" 43 | s += "| Key/Alias | Title | R/W | JSON/API Type | Category | HA Enabled | Description | Example |\n" 44 | s += "|-----------|-------|-----|---------------|----------|------------|-------------|---------|\n" 45 | for p in wpapidef["properties"]: 46 | s += f"| {pk(p)} | {pv(p,'title')} | {pv(p,'rw')} | {pt(p)} | {pv(p,'category')} | {ha(p)} | {pv(p,'description')} | {e(p)} |\n" 47 | 48 | print(s, end='') 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "pyyaml>=5.4.1", 4 | "setuptools>=42", 5 | "wheel", 6 | "paho-mqtt", 7 | "cmd2>=2.4.1", 8 | ] 9 | build-backend = "setuptools.build_meta" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client>=1.2.3 2 | PyYAML>=5.4.1 3 | pyreadline 4 | paho-mqtt 5 | cmd2>=2.4.1 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup, find_packages 3 | import pathlib 4 | 5 | here = pathlib.Path(__file__).parent.resolve() 6 | 7 | # Get the long description from the README file 8 | long_description = (here / 'README.md').read_text(encoding='utf-8') 9 | 10 | # Arguments marked as "Required" below must be included for upload to PyPI. 11 | # Fields marked as "Optional" may be commented out. 12 | 13 | setup( 14 | name='wattpilot', 15 | version='0.2', 16 | description='Python library to connect to a Fronius Wattpilot Wallbox', 17 | long_description=long_description, 18 | long_description_content_type='text/markdown', 19 | url='https://github.com/joscha82/wattpilot', 20 | author='Joscha Arenz', 21 | author_email='joscha@arenz.co', 22 | classifiers=[ 23 | 'Development Status :: 3 - Alpha', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3', 27 | "Programming Language :: Python :: 3.10", 28 | 'Programming Language :: Python :: 3 :: Only', 29 | ], 30 | keywords='wattpilot', 31 | package_dir={'': 'src'}, 32 | packages=find_packages(where='src'), 33 | entry_points = { 34 | 'console_scripts': ['wattpilotshell=wattpilot.wattpilotshell:main'], 35 | }, 36 | package_data = { '' : ['wattpilot.yaml'] }, 37 | python_requires='>=3.10, <4', 38 | install_requires=['websocket-client','PyYAML','paho-mqtt','cmd2'], 39 | platforms="any", 40 | license="MIT License", 41 | project_urls={ 42 | 'Bug Reports': 'https://github.com/joscha82/wattpilot/issues', 43 | 'Source': 'https://github.com/joscha82/wattpilot' 44 | }, 45 | include_package_data=True 46 | ) 47 | -------------------------------------------------------------------------------- /src/wattpilot/__init__.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import json 3 | import hashlib 4 | import random 5 | import threading 6 | import hmac 7 | import logging 8 | import base64 9 | 10 | from enum import Enum, auto 11 | from time import sleep 12 | from types import SimpleNamespace 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | class LoadMode(): 17 | """Wrapper Class to represent the Load Mode of the Wattpilot""" 18 | DEFAULT=3 19 | ECO=4 20 | NEXTTRIP=5 21 | 22 | 23 | class Event(Enum): 24 | # Wattpilot events: 25 | WP_AUTH = auto(), 26 | WP_AUTH_ERROR = auto(), 27 | WP_AUTH_SUCCESS = auto(), 28 | WP_CLEAR_INVERTERS = auto(), 29 | WP_CONNECT = auto(), 30 | WP_DELTA_STATUS = auto(), 31 | WP_DISCONNECT = auto(), 32 | WP_FULL_STATUS = auto(), 33 | WP_FULL_STATUS_FINISHED = auto(), 34 | WP_HELLO = auto(), 35 | WP_INIT = auto(), 36 | WP_PROPERTY = auto(), 37 | WP_RESPONSE = auto(), 38 | WP_UPDATE_INVERTER = auto(), 39 | # WebSocketApp events: 40 | WS_CLOSE = auto(), 41 | WS_ERROR = auto(), 42 | WS_MESSAGE = auto(), 43 | WS_OPEN = auto(), 44 | 45 | class Wattpilot(object): 46 | 47 | carValues = {} 48 | alwValues = {} 49 | astValues = {} 50 | lmoValues = {} 51 | ustValues = {} 52 | errValues = {} 53 | acsValues = {} 54 | 55 | lmoValues[3] = "Default" 56 | lmoValues[4] = "Eco" 57 | lmoValues[5] = "Next Trip" 58 | 59 | astValues[0] = "open" 60 | astValues[1] = "locked" 61 | astValues[2] = "auto" 62 | 63 | carValues[1] = "no car" 64 | carValues[2] = "charging" 65 | carValues[3] = "ready" 66 | carValues[4] = "complete" 67 | 68 | alwValues[0] = False 69 | alwValues[1] = True 70 | 71 | ustValues[0] = "Normal" 72 | ustValues[1] = "AutoUnlock" 73 | ustValues[2] = "AlwaysLock" 74 | 75 | errValues[0] = "Unknown Error" 76 | errValues[1] = "Idle" 77 | errValues[2] = "Charging" 78 | errValues[3] = "Wait Car" 79 | errValues[4] = "Complete" 80 | errValues[5] = "Error" 81 | 82 | acsValues[0] = "Open" 83 | acsValues[1] = "Wait" 84 | 85 | 86 | @property 87 | def allProps(self): 88 | """Returns a dictionary with all properties""" 89 | return self._allProps 90 | 91 | @property 92 | def allPropsInitialized(self): 93 | """Returns true, if all properties have been initialized""" 94 | return self._allPropsInitialized 95 | 96 | @property 97 | def cableType(self): 98 | """Returns the Cable Type (Ampere) of the connected cable""" 99 | return self._cableType 100 | 101 | @property 102 | def frequency(self): 103 | """Returns the power frequency""" 104 | return self._frequency 105 | 106 | @property 107 | def phases(self): 108 | """returns the phases""" 109 | return self._phases 110 | 111 | @property 112 | def energyCounterSinceStart(self): 113 | """Returns used kwh since start of charging""" 114 | return self._energyCounterSinceStart 115 | 116 | @property 117 | def errorState(self): 118 | """Returns error State""" 119 | return self._errorState 120 | 121 | @property 122 | def cableLock(self): 123 | return self._cableLock 124 | 125 | @property 126 | def energyCounterTotal(self): 127 | return self._energyCounterTotal 128 | 129 | @property 130 | def serial(self): 131 | """Returns the serial number of Wattpilot Device (read only)""" 132 | return self._serial 133 | @serial.setter 134 | def serial(self,value): 135 | self._serial = value 136 | if (self._password is not None) & (self._serial is not None): 137 | self._hashedpassword = base64.b64encode(hashlib.pbkdf2_hmac('sha512',self._password.encode(),self._serial.encode(),100000,256))[:32] 138 | 139 | @property 140 | def name(self): 141 | """Returns the name of Wattpilot Device (read only)""" 142 | return self._name 143 | 144 | 145 | @property 146 | def hostname(self): 147 | """Returns the DNS Hostname of Wattpilot Device (read only)""" 148 | return self._hostname 149 | 150 | @property 151 | def friendlyName(self): 152 | """Returns the friendly name of Wattpilot Device (read only)""" 153 | return self._friendlyName 154 | 155 | @property 156 | def manufacturer(self): 157 | """Returns the Manufacturer of Wattpilot Device (read only)""" 158 | return self._manufacturer 159 | 160 | @property 161 | def devicetype(self): 162 | return self._devicetype 163 | 164 | @property 165 | def protocol(self): 166 | return self._protocol 167 | 168 | @property 169 | def secured(self): 170 | return self._secured 171 | 172 | @property 173 | def password(self): 174 | return self._password 175 | @password.setter 176 | def password(self,value): 177 | self._password = value 178 | if (self._password is not None) & (self._serial is not None): 179 | self._hashedpassword = base64.b64encode(hashlib.pbkdf2_hmac('sha512',self._password.encode(),self._serial.encode(),100000,256))[:32] 180 | 181 | 182 | @property 183 | def url(self): 184 | return self._url 185 | @url.setter 186 | def url(self,value): 187 | self._url = value 188 | 189 | @property 190 | def connected(self): 191 | return self._connected 192 | 193 | @property 194 | def voltage1(self): 195 | return self._voltage1 196 | 197 | @property 198 | def voltage2(self): 199 | return self._voltage2 200 | 201 | @property 202 | def voltage3(self): 203 | return self._voltage3 204 | 205 | @property 206 | def voltageN(self): 207 | return self._voltageN 208 | 209 | @property 210 | def amps1(self): 211 | return self._amps1 212 | 213 | @property 214 | def amps2(self): 215 | return self._amps2 216 | 217 | @property 218 | def amps3(self): 219 | return self._amps3 220 | 221 | @property 222 | def power1(self): 223 | return self._power1 224 | 225 | @property 226 | def power2(self): 227 | return self._power2 228 | 229 | @property 230 | def power3(self): 231 | return self._power3 232 | 233 | @property 234 | def powerN(self): 235 | return self._powerN 236 | 237 | @property 238 | def power(self): 239 | return self._power 240 | 241 | @property 242 | def version(self): 243 | return self._version 244 | 245 | @property 246 | def amp(self): 247 | return self._amp 248 | 249 | @property 250 | def AccessState(self): 251 | return self._AccessState 252 | 253 | @property 254 | def firmware(self): 255 | """Returns the Firmwareversion of Wattpilot Device (read only)""" 256 | return self._firmware 257 | 258 | @property 259 | def WifiSSID(self): 260 | """Returns the SSID of the Wifi network currently connected (read only)""" 261 | return self._WifiSSID 262 | 263 | @property 264 | def AllowCharging(self): 265 | return self._AllowCharging 266 | 267 | @property 268 | def mode(self): 269 | return self._mode 270 | 271 | @property 272 | def carConnected(self): 273 | return self._carConnected 274 | 275 | @property 276 | def cae(self): 277 | """Returns true if Cloud API Access is enabled (read only)""" 278 | return self._cae 279 | 280 | @property 281 | def cak(self): 282 | """Returns the API Key for Cloud API Access (read only)""" 283 | return self._cak 284 | 285 | 286 | def __str__(self): 287 | """Returns a String representation of the core Wattpilot attributes""" 288 | if self.connected: 289 | ret = "Wattpilot: " + str(self.name) + "\n" 290 | ret = ret + "Serial: " + str(self.serial) + "\n" 291 | ret = ret + "Connected: " + str(self.connected) + "\n" 292 | ret = ret + "Car Connected: " + str(self.carConnected) + "\n" 293 | ret = ret + "Charge Status " + str(self.AllowCharging) + "\n" 294 | ret = ret + "Mode: " + str(self.mode) + "\n" 295 | ret = ret + "Power: " + str(self.amp) + "\n" 296 | ret = ret + "Charge: " + "%.2f" % self.power + "kW" + " ---- " + str(self.voltage1) + "V/" + str(self.voltage2) + "V/" + str(self.voltage3) + "V" + " -- " 297 | ret = ret + "%.2f" % self.amps1 + "A/" + "%.2f" % self.amps2 + "A/" + "%.2f" % self.amps3 + "A" + " -- " 298 | ret = ret + "%.2f" % self.power1 + "kW/" + "%.2f" % self.power2 + "kW/" + "%.2f" % self.power3 + "kW" + "\n" 299 | else: 300 | ret = "Not connected" 301 | 302 | return ret 303 | def connect(self): 304 | self._wst = threading.Thread(target=self._wsapp.run_forever) 305 | self._wst.daemon = True 306 | self._wst.start() 307 | self.__call_event_handler(Event.WP_CONNECT) 308 | _LOGGER.info("Wattpilot connected") 309 | 310 | def disconnect(self, auto_reconnect=False): 311 | self._wsapp.close() 312 | self._connected=False 313 | self._auto_reconnect = auto_reconnect 314 | self.__call_event_handler(Event.WP_DISCONNECT) 315 | _LOGGER.info("Wattpilot disconnected") 316 | 317 | # Wattpilot Event Handling 318 | 319 | # def __init_event_handler(): 320 | # eh = {} 321 | # for event_type in list(Event): 322 | # eh[event_type.value] = [] 323 | # return eh 324 | 325 | def add_event_handler(self,event_type,callback_fn): 326 | if event_type not in self._event_handler: 327 | self._event_handler[event_type] = [] 328 | self._event_handler[event_type].append(callback_fn) 329 | 330 | def remove_event_handler(self,event_type,callback_fn): 331 | if event_type in self._event_handler and callback_fn in self._event_handler[event_type]: 332 | self._event_handler[event_type].remove(callback_fn) 333 | 334 | def __call_event_handler(self, event_type, *args): 335 | _LOGGER.debug(f"Calling event handler for event type '{event_type} ...") 336 | if event_type not in self._event_handler: 337 | return 338 | for callback_fn in self._event_handler[event_type]: 339 | event = { 340 | "type": event_type, 341 | "wp": self, 342 | } 343 | callback_fn(event,*args) 344 | 345 | 346 | def set_power(self,power): 347 | self.send_update("amp",power) 348 | 349 | def set_mode(self,mode): 350 | self.send_update("lmo",mode) 351 | 352 | 353 | def send_update(self,name,value): 354 | message = {} 355 | message["type"]="setValue" 356 | self.__requestid = self.__requestid+1 357 | message["requestId"]=self.__requestid 358 | message["key"]=name 359 | message["value"]=value 360 | if (self._secured is not None): 361 | if (self._secured > 0): 362 | self.__send(message,True) 363 | else: 364 | self.__send(message) 365 | else: 366 | self.__send(message) 367 | 368 | def unpairInverter(self,InverterID): 369 | message = {} 370 | message["type"]="unpairInverter" 371 | self.__requestid = self.__requestid+1 372 | message["requestId"]=self.__requestid 373 | message["inverterId"]=InverterID 374 | if (self._secured is not None): 375 | if (self._secured > 0): 376 | self.__send(message,True) 377 | else: 378 | self.__send(message) 379 | else: 380 | self.__send(message) 381 | 382 | def pairInverter(self,InverterID): 383 | message = {} 384 | message["type"]="pairInverter" 385 | self.__requestid = self.__requestid+1 386 | message["requestId"]=self.__requestid 387 | message["inverterId"]=InverterID 388 | if (self._secured is not None): 389 | if (self._secured > 0): 390 | self.__send(message,True) 391 | else: 392 | self.__send(message) 393 | else: 394 | self.__send(message) 395 | 396 | def __update_property(self,name,value): 397 | 398 | self._allProps[name] = value 399 | if name=="acs": 400 | self._AccessState = Wattpilot.acsValues[value] 401 | 402 | if name=="cbl": 403 | self._cableType = value 404 | 405 | if name=="fhz": 406 | self._frequency = value 407 | 408 | if name=="pha": 409 | self._phases = value 410 | 411 | if name=="wh": 412 | self._energyCounterSinceStart = value 413 | 414 | if name=="err": 415 | self._errorState = Wattpilot.errValues[value] 416 | 417 | if name=="ust": 418 | self._cableLock = Wattpilot.ustValues[value] 419 | 420 | if name=="eto": 421 | self._energyCounterTotal = value 422 | 423 | if name=="cae": 424 | self._cae = value 425 | if name=="cak": 426 | self._cak = value 427 | if name=="lmo": 428 | self._mode = Wattpilot.lmoValues[value] 429 | if name=="car": 430 | self._carConnected = Wattpilot.carValues[value] 431 | if name=="alw": 432 | self._AllowCharging = Wattpilot.alwValues[value] 433 | if name=="nrg": 434 | self._voltage1=value[0] 435 | self._voltage2=value[1] 436 | self._voltage3=value[2] 437 | self._voltageN=value[3] 438 | self._amps1=value[4] 439 | self._amps2=value[5] 440 | self._amps3=value[6] 441 | self._power1=value[7]*0.001 442 | self._power2=value[8]*0.001 443 | self._power3=value[9]*0.001 444 | self._powerN=value[10]*0.001 445 | self._power=value[11]*0.001 446 | if name=="amp": 447 | self._amp = value 448 | if name=="version": 449 | self._version = value 450 | if name=="ast": 451 | self._AllowCharging = self._astValues[value] 452 | if name=="fwv": 453 | self._firmware = value 454 | if name=="wss": 455 | self._WifiSSID=value 456 | if name=="upd": 457 | if value=="0": 458 | self._updateAvailable = False 459 | else: 460 | self._updateAvailable = True 461 | self.__call_event_handler(Event.WP_PROPERTY, name, value) 462 | 463 | def __on_hello(self,message): 464 | _LOGGER.info("Connected to WattPilot Serial %s",message.serial) 465 | if hasattr(message,"hostname"): 466 | self._name=message.hostname 467 | self.serial = message.serial 468 | if hasattr(message,"hostname"): 469 | self._hostname=message.hostname 470 | if hasattr(message,"version"): 471 | self._version=message.version 472 | self._manufacturer=message.manufacturer 473 | self._devicetype=message.devicetype 474 | self._protocol=message.protocol 475 | if hasattr(message,"secured"): 476 | self._secured=message.secured 477 | self.__call_event_handler(Event.WP_HELLO, message) 478 | 479 | def __on_auth(self,wsapp,message): 480 | ran = random.randrange(10**80) 481 | self._token3 = "%064x" % ran 482 | self._token3 = self._token3[:32] 483 | hash1 = hashlib.sha256((message.token1.encode()+self._hashedpassword)).hexdigest() 484 | hash = hashlib.sha256((self._token3 + message.token2+hash1).encode()).hexdigest() 485 | response = {} 486 | response["type"] = "auth" 487 | response["token3"] = self._token3 488 | response["hash"] = hash 489 | self.__send(response) 490 | self.__call_event_handler(Event.WP_AUTH, message) 491 | 492 | def __send(self,message,secure=False): 493 | # If the connection to wattpilot is over a unsecure channel (http) all send messages are wrapped in 494 | # a "securedMsg" Message which contains the original messageobject and a sha256 HMAC Hashed created 495 | # using the password 496 | if secure: 497 | messageid=message["requestId"] 498 | payload=json.dumps(message) 499 | h = hmac.new(bytearray(self._hashedpassword), bytearray(payload.encode()), hashlib.sha256 ) 500 | message={} 501 | message["type"]="securedMsg" 502 | message["data"]=payload 503 | message["requestId"]=str(messageid)+"sm" 504 | message["hmac"]=h.hexdigest() 505 | 506 | _LOGGER.debug("Message send: %s",json.dumps(message) ) 507 | self._wsapp.send(json.dumps(message)) 508 | 509 | def __on_AuthSuccess(self,message): 510 | self._connected = True 511 | self.__call_event_handler(Event.WP_AUTH_SUCCESS, message) 512 | _LOGGER.info("Authentication successful") 513 | 514 | def __on_FullStatus(self,message): 515 | props = message.status.__dict__ 516 | for key in props: 517 | self.__update_property(key,props[key]) 518 | self.__call_event_handler(Event.WP_FULL_STATUS, message) 519 | self._allPropsInitialized = not message.partial 520 | if message.partial == False: 521 | self.__call_event_handler(Event.WP_FULL_STATUS_FINISHED, message) 522 | 523 | def __on_AuthError(self,message): 524 | if message.message=="Wrong password": 525 | self._wsapp.close() 526 | _LOGGER.error("Authentication failed: %s", message.message) 527 | self.__call_event_handler(Event.WP_AUTH_ERROR, message) 528 | 529 | def __on_DeltaStatus(self,message): 530 | props = message.status.__dict__ 531 | for key in props: 532 | self.__update_property(key,props[key]) 533 | self.__call_event_handler(Event.WP_DELTA_STATUS, message) 534 | 535 | def __on_clearInverters(self,message): 536 | self.__call_event_handler(Event.WP_CLEAR_INVERTERS, message) 537 | 538 | def __on_updateInverter(self,message): 539 | self.__call_event_handler(Event.WP_UPDATE_INVERTER, message) 540 | 541 | def __on_response(self,message): 542 | if message.success: 543 | if hasattr(message,"status"): 544 | props = message.status.__dict__ 545 | for key in props: 546 | self.__update_property(key,props[key]) 547 | else: 548 | _LOGGER.error("Error Sending Request %s. Message: %s" ,message.requestId,message.message) 549 | self.__call_event_handler(Event.WP_RESPONSE, message) 550 | 551 | def __on_open(self,wsapp): 552 | self.__call_event_handler(Event.WS_OPEN, wsapp) 553 | 554 | def __on_error(self,wsapp,err): 555 | self.__call_event_handler(Event.WS_ERROR, wsapp, err) 556 | _LOGGER.error(f"Error received from WebSocketApp: {err}") 557 | 558 | def __on_close(self,wsapp,code,msg): 559 | self._connected=False 560 | self.__call_event_handler(Event.WS_CLOSE, wsapp, code, msg) 561 | if (self._auto_reconnect): 562 | sleep(self._reconnect_interval) 563 | self._wsapp.run_forever() 564 | 565 | def __on_message(self, wsapp, message): 566 | ## called whenever a message through websocket is received 567 | _LOGGER.debug("Message received: %s", message) 568 | msg=json.loads(message, object_hook=lambda d: SimpleNamespace(**d)) 569 | self.__call_event_handler(Event.WS_MESSAGE, message) 570 | if (msg.type == 'hello'): # Hello Message -> Received upon connection before auth 571 | self.__on_hello(msg) 572 | if (msg.type == 'authRequired'): # Auth Required -> Received after hello 573 | self.__on_auth(wsapp,msg) 574 | if (msg.type == 'response'): # Response Message -> Received after sending a update and contains result of update 575 | self.__on_response(msg) 576 | if (msg.type == 'authSuccess'): # Auth Success -> Received after sending correct authentication message 577 | self.__on_AuthSuccess(msg) 578 | if (msg.type == 'authError'): # Auth Error -> Received after sending incorrect authentication message (e.g. wrong password) 579 | self.__on_AuthError(msg) 580 | if (msg.type == 'fullStatus'): # Full Status -> Received after successful connection. Contains all properties of Wattpilot 581 | self.__on_FullStatus(msg) 582 | if (msg.type == 'deltaStatus'): # Delta Status -> Whenever a property changes a Delta Status is send 583 | self.__on_DeltaStatus(msg) 584 | if (msg.type == 'clearInverters'): # Unknown 585 | self.__on_clearInverters(msg) 586 | if (msg.type == 'updateInverter'): # Contains information of connected Photovoltaik inverter / powermeter 587 | self.__on_updateInverter(msg) 588 | 589 | def __init__(self, ip ,password,serial=None,cloud=False): 590 | self._auto_reconnect = True 591 | self._reconnect_interval = 30 592 | self._websocket_default_timeout = 10 593 | self.__requestid = 0 594 | self._name = None 595 | self._hostname = None 596 | self._friendlyName = None 597 | self._manufacturer = None 598 | self._devicetype = None 599 | self._protocol = None 600 | self._secured = None 601 | self._serial = None 602 | self._password = None 603 | 604 | self.password = password 605 | 606 | if(cloud): 607 | self._url= "wss://app.wattpilot.io/app/" + serial + "?version=1.2.9" 608 | else: 609 | self._url = "ws://"+ip+"/ws" 610 | self.serial = None 611 | self._connected = False 612 | self._allProps={} 613 | self._allPropsInitialized=False 614 | self._voltage1=None 615 | self._voltage2=None 616 | self._voltage3=None 617 | self._voltageN=None 618 | self._amps1=None 619 | self._amps2=None 620 | self._amps3=None 621 | self._power1=None 622 | self._power2=None 623 | self._power3=None 624 | self._powerN=None 625 | self._power=None 626 | self._version = None 627 | self._amp = None 628 | self._AccessState = None 629 | self._firmware = None 630 | self._WifiSSID = None 631 | self._AllowCharging = None 632 | self._mode=None 633 | self._carConnected=None 634 | self._cae=None 635 | self._cak=None 636 | self._event_handler = {} 637 | 638 | self._wst=threading.Thread() 639 | 640 | websocket.setdefaulttimeout(self._websocket_default_timeout) 641 | self._wsapp = websocket.WebSocketApp( 642 | self.url, 643 | on_close=self.__on_close, 644 | on_error=self.__on_error, 645 | on_message=self.__on_message, 646 | on_open=self.__on_open, 647 | ) 648 | self.__call_event_handler(Event.WP_INIT) 649 | _LOGGER.info ("Wattpilot %s initialized",self.serial) 650 | 651 | -------------------------------------------------------------------------------- /src/wattpilot/ressources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joscha82/wattpilot/4712ba3b8409fda55303870c047038b1b221d7ff/src/wattpilot/ressources/__init__.py -------------------------------------------------------------------------------- /src/wattpilot/wattpilotshell.py: -------------------------------------------------------------------------------- 1 | import cmd2 2 | import json 3 | import logging 4 | import math 5 | import os 6 | import paho.mqtt.client as mqtt 7 | import re 8 | import sys 9 | import wattpilot 10 | import yaml 11 | import pkgutil 12 | 13 | from enum import Enum, auto 14 | from importlib.metadata import version 15 | from time import sleep 16 | from threading import Event 17 | from types import SimpleNamespace 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | #### Utility Functions #### 23 | 24 | def utils_add_to_dict_unique(d, k, v): 25 | if k in d: 26 | _LOGGER.warning( 27 | f"About to add duplicate key {k} to dictionary - skipping!") 28 | else: 29 | d[k] = v 30 | return d 31 | 32 | 33 | def utils_wait_timeout(fn, timeout): 34 | """Generic timeout waiter""" 35 | t = 0 36 | within_timeout = True 37 | while not fn() and t < timeout: 38 | sleep(1) 39 | t += 1 40 | if t >= timeout: 41 | within_timeout = False 42 | return within_timeout 43 | 44 | 45 | class JSONNamespaceEncoder(json.JSONEncoder): 46 | # See https://gist.github.com/jdthorpe/313cafc6bdaedfbc7d8c32fcef799fbf 47 | def default(self, obj): 48 | if isinstance(obj, SimpleNamespace): 49 | return obj.__dict__ 50 | return super(JSONNamespaceEncoder, self).default(obj) 51 | 52 | 53 | def utils_value2json(value): 54 | return json.dumps(value, cls=JSONNamespaceEncoder) 55 | 56 | 57 | #### Wattpilot Functions #### 58 | 59 | def wp_read_apidef(): 60 | api_definition = pkgutil.get_data(__name__, "ressources/wattpilot.yaml") 61 | wpdef = { 62 | "config": {}, 63 | "messages": {}, 64 | "properties:": {}, 65 | "splitProperties": [], 66 | } 67 | try: 68 | wpdef["config"] = yaml.safe_load(api_definition) 69 | wpdef["messages"] = dict(zip( 70 | [x["key"] for x in wpdef["config"]["messages"]], 71 | [x for x in wpdef["config"]["messages"]], 72 | )) 73 | wpdef["properties"] = {} 74 | for p in wpdef["config"]["properties"]: 75 | wpdef["properties"] = utils_add_to_dict_unique( 76 | wpdef["properties"], p["key"], p) 77 | if "childProps" in p and Cfg.WATTPILOT_SPLIT_PROPERTIES.val: 78 | for cp in p["childProps"]: 79 | cp = { 80 | # Defaults for split properties: 81 | "description": f"This is a child property of '{p['key']}'. See its description for more information.", 82 | "category": p["category"] if "category" in p else "", 83 | "jsonType": p["itemType"] if "itemType" in p else "", 84 | } | cp | { 85 | # Overrides for split properties: 86 | "parentProperty": p["key"], 87 | "rw": "R", # NOTE: Split properties currently can only be read 88 | } 89 | _LOGGER.debug(f"Adding child property {cp['key']}: {cp}") 90 | wpdef["properties"] = utils_add_to_dict_unique( 91 | wpdef["properties"], cp["key"], cp) 92 | wpdef["splitProperties"].append(cp["key"]) 93 | _LOGGER.debug( 94 | f"Resulting properties config:\n{utils_value2json(wpdef['properties'])}") 95 | except yaml.YAMLError as exc: 96 | _LOGGER.fatal(exc) 97 | return wpdef 98 | 99 | 100 | def wp_initialize(host, password): 101 | global wp 102 | # Connect to Wattpilot: 103 | _LOGGER.debug(f"wp_initialize()") 104 | wp = wattpilot.Wattpilot(host, password) 105 | wp._auto_reconnect = Cfg.WATTPILOT_AUTO_RECONNECT.val 106 | wp._reconnect_interval = Cfg.WATTPILOT_RECONNECT_INTERVAL.val 107 | wp.add_event_handler(wattpilot.Event.WS_CLOSE, wp_handle_events) 108 | wp.add_event_handler(wattpilot.Event.WS_OPEN, wp_handle_events) 109 | return wp 110 | 111 | def wp_connect(wp, wait_for_timeouts=True): 112 | wp.connect() 113 | # Wait for connection and initialization - TODO: Use event handler instead to make it more responsive! 114 | if wait_for_timeouts: 115 | utils_wait_timeout(lambda: wp.connected, Cfg.WATTPILOT_CONNECT_TIMEOUT.val) or exit( 116 | "ERROR: Timeout while connecting to Wattpilot!") 117 | utils_wait_timeout(lambda: wp.allPropsInitialized, Cfg.WATTPILOT_INIT_TIMEOUT.val) or exit( 118 | "ERROR: Timeout while waiting for property initialization!") 119 | return wp 120 | 121 | 122 | def wp_handle_events(event, *args): 123 | global mqtt_client 124 | _LOGGER.debug(f"wp_handle_events(event={event},{args})") 125 | if not mqtt_client: 126 | _LOGGER.debug(f"wp_handle_events(): MQTT client not yet initialized - status publishing skipped.") 127 | return 128 | available_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}) 129 | if event['type'] == 'on_close': 130 | mqtt_client.publish(available_topic, payload="offline", qos=0, retain=True) 131 | elif event['type'] == 'on_open': 132 | mqtt_client.publish(available_topic, payload="online", qos=0, retain=True) 133 | 134 | 135 | def wp_get_child_prop_value(cp): 136 | global wpdef 137 | cpd = wpdef["properties"][cp] 138 | if "parentProperty" not in cpd: 139 | _LOGGER.warning( 140 | f"Child property '{cpd['key']}' is not linked to a parent property: {cpd}") 141 | return None 142 | ppd = wpdef["properties"][cpd["parentProperty"]] 143 | parent_value = wp.allProps[ppd["key"]] 144 | value = None 145 | if ppd["jsonType"] == "array": 146 | value = parent_value[int(cpd["valueRef"])] if int( 147 | cpd["valueRef"]) < len(parent_value) else None 148 | _LOGGER.debug(f" -> got array value {value}") 149 | elif ppd["jsonType"] == "object": 150 | if parent_value == None: 151 | value = None 152 | _LOGGER.debug(f" -> parent value is None, so child as well") 153 | elif isinstance(parent_value, SimpleNamespace) and cpd["valueRef"] in parent_value.__dict__: 154 | value = parent_value.__dict__[cpd["valueRef"]] 155 | _LOGGER.debug(f" -> got object value {value}") 156 | elif cpd["valueRef"] in parent_value: 157 | value = parent_value[cpd["valueRef"]] 158 | _LOGGER.debug(f" -> got object value {value}") 159 | else: 160 | _LOGGER.warning( 161 | f"Unable to map child property {cpd['key']}: type={type(parent_value)}, value={utils_value2json(parent_value)}") 162 | else: 163 | _LOGGER.warning(f"Property {ppd['key']} cannot be split!") 164 | return value 165 | 166 | 167 | def wp_get_all_props(available_only=True): 168 | global wp 169 | global wpdef 170 | if available_only: 171 | props = {k: v for k, v in wp.allProps.items()} 172 | if Cfg.WATTPILOT_SPLIT_PROPERTIES.val: 173 | for cp_key in wpdef["splitProperties"]: 174 | props[cp_key] = wp_get_child_prop_value(cp_key) 175 | else: 176 | props = {k: (wp.allProps[k] if k in wp.allProps else None) 177 | for k in wpdef["properties"].keys()} 178 | return props 179 | 180 | 181 | #### Shell Functions #### 182 | 183 | class WattpilotShell(cmd2.Cmd): 184 | intro = f"Welcome to the Wattpilot Shell {version('wattpilot')}. Type help or ? to list commands.\n" 185 | prompt = 'wattpilot> ' 186 | file = None 187 | watching_messages = [] 188 | watching_properties = [] 189 | 190 | def __init__(self, wp, wpdef): 191 | super().__init__() 192 | self.wp = wp 193 | self.wpdef = wpdef 194 | 195 | def postloop(self) -> None: 196 | print() 197 | return super().postloop() 198 | 199 | def emptyline(self) -> bool: 200 | return False 201 | 202 | def _complete_list(self, clist, text): 203 | return [x for x in clist if x.startswith(text)] 204 | 205 | def _complete_message(self, text, sender=None): 206 | return [md["key"] for md in self.wpdef["messages"].values() if (not sender or md["sender"] == sender) and md["key"].startswith(text)] 207 | 208 | def _complete_propname(self, text, rw=False, available_only=True): 209 | return [k for k in wp_get_all_props(available_only).keys() if (not rw or ("rw" in self.wpdef["properties"][k] and self.wpdef["properties"][k]["rw"] == "R/W")) and k.startswith(text)] 210 | 211 | def _complete_values(self, text, line): 212 | token = line.split(' ') 213 | if len(token) == 2: 214 | return self._complete_propname(text, rw=False, available_only=True) + [''] 215 | elif len(token) == 3 and text in self.wpdef["properties"]: 216 | return ['', ''] 217 | return [] 218 | 219 | def do_EOF(self, arg: str) -> bool | None: 220 | """Exit the shell""" 221 | return True 222 | 223 | def do_connect(self, arg: str) -> bool | None: 224 | """Connect to Wattpilot 225 | Usage: connect""" 226 | wp_connect(self.wp) 227 | 228 | def do_disconnect(self, arg: str) -> bool | None: 229 | """Disconnect from Wattpilot 230 | Usage: disconnect""" 231 | wp.disconnect() 232 | 233 | def do_docs(self, arg: str) -> bool | None: 234 | """Show markdown documentation for environment variables 235 | Usage: docs""" 236 | Cfg.docs_markdown() 237 | 238 | def do_config(self, arg: str) -> bool | None: 239 | """Show configuration values 240 | Usage: config""" 241 | for e in list(Cfg): 242 | #print(f"{e.name}={os.environ.get(e.name,'')} (-> {e.val})") 243 | print(e.value.format()) 244 | 245 | def do_exit(self, arg: str) -> bool | None: 246 | """Exit the shell 247 | Usage: exit""" 248 | return True 249 | 250 | def do_propget(self, arg: str) -> bool | None: 251 | """Get a property value 252 | Usage: propget """ 253 | args = arg.split(' ') 254 | if not self._ensure_connected(): 255 | return 256 | if len(args) < 1 or arg == '': 257 | print(f"ERROR: Wrong number of arguments!") 258 | elif args[0] in self.wp.allProps: 259 | pd = self.wpdef["properties"][args[0]] 260 | print(mqtt_get_encoded_property(pd, self.wp.allProps[args[0]])) 261 | elif args[0] in self.wpdef["splitProperties"]: 262 | pd = self.wpdef["properties"][args[0]] 263 | print(mqtt_get_encoded_property( 264 | pd, wp_get_child_prop_value(pd["key"]))) 265 | else: 266 | print(f"ERROR: Unknown property: {args[0]}") 267 | 268 | def complete_propget(self, text, line, begidx, endidx): 269 | return self._complete_propname(text, rw=False, available_only=True) 270 | 271 | def do_ha(self, arg: str) -> bool | None: 272 | """Control Home Assistant discovery (+MQTT client) 273 | Usage: ha [args...] 274 | 275 | Home Assistant commands: 276 | enable 277 | Enable a discovered entity representing the property 278 | NOTE: Re-enabling of disabled entities may still be broken in HA and require a restart of HA. 279 | disable 280 | Disable a discovered entity representing the property 281 | discover 282 | Let HA discover an entity representing the property 283 | properties 284 | List properties activated for HA discovery 285 | start 286 | Start HA MQTT discovery (using HA_* env variables) 287 | status 288 | Status of HA MQTT discovery 289 | stop 290 | Stop HA MQTT discovery 291 | undiscover 292 | Let HA remove a discovered entity representing the property 293 | NOTE: Removing of disabled entities may still be broken in HA and require a restart of HA. 294 | """ 295 | global mqtt_client 296 | args = arg.split(' ') 297 | if not self._ensure_connected(): 298 | return 299 | if len(args) < 1 or arg == '': 300 | print(f"ERROR: Wrong number of arguments!") 301 | return 302 | if args[0] == "properties": 303 | print( 304 | f"List of properties activated for discovery: {Cfg.HA_PROPERTIES.val}") 305 | elif args[0] == "start": 306 | Cfg.HA_ENABLED.val = True 307 | mqtt_client = ha_setup(wp) 308 | elif args[0] == "stop": 309 | ha_stop(mqtt_client) 310 | Cfg.HA_ENABLED.val = False 311 | elif args[0] == "status": 312 | print( 313 | f"HA discovery is {'enabled' if Cfg.HA_ENABLED.val else 'disabled'}.") 314 | elif len(args) > 1 and args[0] in ['enable', 'disable', 'discover', 'undiscover']: 315 | self._ha_prop_cmds(args[0], args[1]) 316 | else: 317 | print(f"ERROR: Unsupported argument: {args[0]}") 318 | 319 | def _ha_prop_cmds(self, cmd, prop_name): 320 | global mqtt_client 321 | if prop_name not in wpdef["properties"]: 322 | print(f"ERROR: Unknown property '{prop_name}!") 323 | elif cmd == "enable": 324 | if prop_name not in Cfg.MQTT_PROPERTIES.val: 325 | Cfg.MQTT_PROPERTIES.val.append(prop_name) 326 | ha_discover_property( 327 | self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) 328 | elif cmd == "disable": 329 | if prop_name in Cfg.MQTT_PROPERTIES.val: 330 | Cfg.MQTT_PROPERTIES.val.remove(prop_name) 331 | ha_discover_property( 332 | self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=False) 333 | elif cmd == "discover": 334 | if prop_name not in Cfg.HA_PROPERTIES.val: 335 | Cfg.HA_PROPERTIES.val.append(prop_name) 336 | if prop_name not in Cfg.MQTT_PROPERTIES.val: 337 | Cfg.MQTT_PROPERTIES.val.append(prop_name) 338 | ha_discover_property( 339 | self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) 340 | elif cmd == "undiscover": 341 | if prop_name in Cfg.HA_PROPERTIES.val: 342 | Cfg.HA_PROPERTIES.val.remove(prop_name) 343 | if prop_name in Cfg.MQTT_PROPERTIES.val: 344 | Cfg.MQTT_PROPERTIES.val.remove(prop_name) 345 | ha_discover_property( 346 | self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=True, force_enablement=False) 347 | 348 | def complete_ha(self, text, line, begidx, endidx): 349 | token = line.split(' ') 350 | if len(token) == 2: 351 | return self._complete_list(['enable', 'disable', 'discover', 'properties', 'start', 'status', 'stop', 'undiscover'], text) 352 | elif len(token) == 3 and token[1] == 'discover': 353 | return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in Cfg.HA_PROPERTIES.val], text) 354 | elif len(token) == 3 and token[1] in ['enable', 'disable', 'undiscover']: 355 | return self._complete_list(Cfg.HA_PROPERTIES.val, text) 356 | return [] 357 | 358 | def do_info(self, arg: str) -> bool | None: 359 | """Print device infos 360 | Usage: info""" 361 | if not self._ensure_connected(): 362 | return 363 | print(self.wp) 364 | 365 | def do_mqtt(self, arg: str) -> bool | None: 366 | """Control the MQTT bridge 367 | Usage: mqtt [args...] 368 | 369 | MQTT commands: 370 | properties 371 | List properties activated for MQTT publishing 372 | publish 373 | Enable publishing of messages or properties 374 | publish 375 | Enable publishing of a certain message type 376 | publish 377 | Enable publishing of a certain property 378 | start 379 | Start the MQTT bridge (using MQTT_* env variables) 380 | status 381 | Status of the MQTT bridge 382 | stop 383 | Stop the MQTT bridge 384 | unpublish 385 | Disable publishing of messages or properties 386 | unpublish 387 | Disable publishing of a certain message type 388 | unpublish 389 | Disable publishing of a certain property 390 | """ 391 | global mqtt_client 392 | args = arg.split(' ') 393 | if not self._ensure_connected(): 394 | return 395 | if len(args) < 1 or arg == '': 396 | print(f"ERROR: Wrong number of arguments!") 397 | return 398 | if args[0] == "properties": 399 | print( 400 | f"List of properties activated for MQTT publishing: {Cfg.MQTT_PROPERTIES.val}") 401 | elif args[0] == "start": 402 | Cfg.MQTT_ENABLED.val = True 403 | mqtt_client = mqtt_setup(self.wp) 404 | elif args[0] == "stop": 405 | mqtt_stop(mqtt_client) 406 | Cfg.MQTT_ENABLED.val = False 407 | elif args[0] == "status": 408 | print( 409 | f"MQTT client is {'enabled' if Cfg.MQTT_ENABLED.val else 'disabled'}.") 410 | elif len(args) > 1 and args[0] in ['publish', 'unpublish']: 411 | self._mqtt_prop_cmds(args[0], args[1]) 412 | else: 413 | print(f"ERROR: Unsupported argument: {args[0]}") 414 | 415 | def _mqtt_prop_cmds(self, cmd, prop_name): 416 | global mqtt_client 417 | if prop_name not in self.wpdef["properties"]: 418 | print(f"ERROR: Undefined property '{prop_name}'!") 419 | elif cmd == "publish" and prop_name not in Cfg.MQTT_PROPERTIES.val: 420 | Cfg.MQTT_PROPERTIES.val.append(prop_name) 421 | elif cmd == "unpublish" and prop_name in Cfg.MQTT_PROPERTIES.val: 422 | Cfg.MQTT_PROPERTIES.val.remove(prop_name) 423 | 424 | def complete_mqtt(self, text, line, begidx, endidx): 425 | token = line.split(' ') 426 | if len(token) == 2: 427 | return self._complete_list(['properties', 'publish', 'start', 'status', 'stop', 'unpublish'], text) 428 | elif len(token) == 3 and token[1] == 'publish': 429 | return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in Cfg.MQTT_PROPERTIES.val], text) 430 | elif len(token) == 3 and token[1] == 'unpublish': 431 | return self._complete_list(Cfg.MQTT_PROPERTIES.val, text) 432 | return [] 433 | 434 | def do_properties(self, arg: str) -> bool | None: 435 | """List property definitions and values 436 | Usage: properties [propRegex]""" 437 | if not self._ensure_connected(): 438 | return 439 | props = self._get_props_matching_regex(arg, available_only=False) 440 | if not props: 441 | print(f"No matching properties found!") 442 | return 443 | print(f"Properties:") 444 | for prop_name, value in sorted(props.items()): 445 | self._print_prop_info(self.wpdef["properties"][prop_name], value) 446 | print() 447 | 448 | def complete_properties(self, text, line, begidx, endidx): 449 | return self._complete_propname(text, rw=False, available_only=False) + [''] 450 | 451 | def do_rawvalues(self, arg: str) -> bool | None: 452 | """List raw values of properties (without value mapping) 453 | Usage: rawvalues [propRegex] [valueRegex]""" 454 | if not self._ensure_connected(): 455 | return 456 | print(f"List raw values of properties (without value mapping):") 457 | props = self._get_props_matching_regex(arg) 458 | for pd, value in sorted(props.items()): 459 | print(f"- {pd}: {utils_value2json(value)}") 460 | print() 461 | 462 | def complete_rawvalues(self, text, line, begidx, endidx): 463 | return self._complete_values(text, line) 464 | 465 | def do_server(self, arg: str) -> bool | None: 466 | """Start in server mode (infinite wait loop) 467 | Usage: server""" 468 | if not self._ensure_connected(): 469 | return 470 | _LOGGER.info("Server started.") 471 | try: 472 | Event().wait() 473 | except KeyboardInterrupt: 474 | _LOGGER.info("Server shutting down.") 475 | return True 476 | 477 | def do_propset(self, arg: str) -> bool | None: 478 | """Set a property value 479 | Usage: propset """ 480 | args = arg.split(' ') 481 | if not self._ensure_connected(): 482 | return 483 | if len(args) < 2 or arg == '': 484 | print(f"ERROR: Wrong number of arguments!") 485 | elif args[0] not in wp.allProps: 486 | print(f"ERROR: Unknown property: {args[0]}") 487 | else: 488 | if args[1].lower() in ["false", "true"]: 489 | v = json.loads(args[1].lower()) 490 | elif str(args[1]).isnumeric(): 491 | v = int(args[1]) 492 | elif str(args[1]).isdecimal(): 493 | v = float(args[1]) 494 | else: 495 | v = str(args[1]) 496 | wp.send_update(args[0], mqtt_get_decoded_property( 497 | self.wpdef["properties"][args[0]], v)) 498 | 499 | def do_UpdateInverter(self, arg: str) -> bool | None: 500 | """Performs an Inverter Operation 501 | Usage: updateInverter pair|unpair 502 | is normally in the form 123.456789""" 503 | global wp 504 | global wpdef 505 | args = arg.split(' ') 506 | if not self._ensure_connected(): 507 | return 508 | if len(args) < 2 or arg == '': 509 | print(f"ERROR: Wrong number of arguments!") 510 | elif args[0] not in ["pair", "unpair"]: 511 | print(f"ERROR: Unknown Operation: {args[0]}") 512 | else: 513 | if args[0] == 'pair': 514 | wp.pairInverter(args[1]) 515 | if args[0] == 'unpair': 516 | wp.unpairInverter(args[1]) 517 | 518 | 519 | def complete_propset(self, text, line, begidx, endidx): 520 | token = line.split(' ') 521 | if len(token) == 2: 522 | return self._complete_propname(text, rw=True, available_only=True) 523 | elif len(token) == 3 and token[1] in self.wpdef["properties"]: 524 | pd = self.wpdef["properties"][token[1]] 525 | if "jsonType" in pd and pd["jsonType"] == 'boolean': 526 | return [v for v in ['false', 'true'] if v.startswith(text)] 527 | elif "valueMap" in pd: 528 | return [v for v in pd["valueMap"].values() if v.startswith(text)] 529 | elif "jsonType" in pd: 530 | return [f"<{pd['jsonType']}>"] 531 | return [] 532 | 533 | def do_unwatch(self, arg: str) -> bool | None: 534 | """Unwatch a message or property 535 | Usage: unwatch """ 536 | args = arg.split(' ') 537 | if len(args) < 2 or arg == '': 538 | print(f"ERROR: Wrong number of arguments!") 539 | elif args[0] == 'event' and args[1] not in [e.name for e in list(wattpilot.Event)]: 540 | print(f"ERROR: Event of type '{args[1]}' is not watched") 541 | elif args[0] == 'event': 542 | self.wp.remove_event_handler(wattpilot.Event[args[1]], self._watched_event_received) 543 | elif args[0] == 'message' and args[1] not in self.watching_messages: 544 | print(f"ERROR: Message of type '{args[1]}' is not watched") 545 | elif args[0] == 'message': 546 | self.watching_messages.remove(args[1]) 547 | if len(self.watching_messages) == 0: 548 | self.wp.remove_event_handler(wattpilot.Event.WS_MESSAGE,self._watched_message_received) 549 | elif args[0] == 'property' and args[1] not in self.watching_properties: 550 | print(f"ERROR: Property with name '{args[1]}' is not watched") 551 | elif args[0] == 'property': 552 | self.watching_properties.remove(args[1]) 553 | if len(self.watching_properties) == 0: 554 | self.wp.remove_event_handler(wattpilot.Event.WP_PROPERTY,self._watched_property_changed) 555 | else: 556 | print(f"ERROR: Unknown watch type: {args[0]}") 557 | 558 | def complete_unwatch(self, text, line, begidx, endidx): 559 | token = line.split(' ') 560 | if len(token) == 2: 561 | return self._complete_list(['event', 'message', 'property'], text) 562 | elif len(token) == 3 and token[1] == 'event': 563 | return self._complete_list([e.name for e in list(wattpilot.Event) if len(wp._event_handler[e])>0], text) 564 | elif len(token) == 3 and token[1] == 'message': 565 | return self._complete_list(self.watching_messages, text) 566 | elif len(token) == 3 and token[1] == 'property': 567 | return self._complete_list(self.watching_properties, text) 568 | return [] 569 | 570 | def do_values(self, arg: str) -> bool | None: 571 | """List values of properties (with value mapping enabled) 572 | Usage: values [propRegex] [valueRegex]""" 573 | if not self._ensure_connected(): 574 | return 575 | print(f"List values of properties (with value mapping):") 576 | props = self._get_props_matching_regex(arg) 577 | for pd, value in sorted(props.items()): 578 | print( 579 | f"- {pd}: {mqtt_get_encoded_property(self.wpdef['properties'][pd],value)}") 580 | print() 581 | 582 | def complete_values(self, text, line, begidx, endidx): 583 | return self._complete_values(text, line) 584 | 585 | def do_watch(self, arg: str) -> bool | None: 586 | """Watch an event, a message or a property 587 | Usage: watch """ 588 | args = arg.split(' ') 589 | if len(args) < 2 or arg == '': 590 | print(f"ERROR: Wrong number of arguments!") 591 | elif args[0] == 'event' and args[1] not in [e.name for e in list(wattpilot.Event)]: 592 | print(f"ERROR: Unknown event type: {args[1]}") 593 | elif args[0] == 'event': 594 | self.wp.add_event_handler(wattpilot.Event[args[1]], self._watched_event_received) 595 | elif args[0] == 'message' and args[1] not in self.wpdef['messages']: 596 | print(f"ERROR: Unknown message type: {args[1]}") 597 | elif args[0] == 'message': 598 | msg_type = args[1] 599 | if len(self.watching_messages) == 0: 600 | self.wp.add_event_handler(wattpilot.Event.WS_MESSAGE,self._watched_message_received) 601 | if msg_type not in self.watching_messages: 602 | self.watching_messages.append(msg_type) 603 | elif args[0] == 'property' and args[1] not in wp.allProps: 604 | print(f"ERROR: Unknown property: {args[1]}") 605 | elif args[0] == 'property': 606 | prop_name = args[1] 607 | if len(self.watching_properties) == 0: 608 | wp.add_event_handler(wattpilot.Event.WP_PROPERTY,self._watched_property_changed) 609 | if prop_name not in self.watching_properties: 610 | self.watching_properties.append(prop_name) 611 | else: 612 | print(f"ERROR: Unknown watch type: {args[0]}") 613 | 614 | def complete_watch(self, text, line, begidx, endidx): 615 | token = line.split(' ') 616 | if len(token) == 2: 617 | return self._complete_list(['event', 'message', 'property'], text) 618 | elif len(token) == 3 and token[1] == 'event': 619 | return self._complete_list([e.name for e in list(wattpilot.Event)], text) 620 | elif len(token) == 3 and token[1] == 'message': 621 | return self._complete_message(text, 'server') 622 | elif len(token) == 3 and token[1] == 'property': 623 | return self._complete_propname(text, rw=False, available_only=True) + [''] 624 | return [] 625 | 626 | def _print_prop_info(self, pd, value): 627 | _LOGGER.debug(f"Property definition: {pd}") 628 | title = "" 629 | desc = "" 630 | alias = "" 631 | rw = "" 632 | if 'alias' in pd: 633 | alias = f", alias:{pd['alias']}" 634 | if 'rw' in pd: 635 | rw = f", rw:{pd['rw']}" 636 | if 'title' in pd: 637 | title = pd['title'] 638 | if 'description' in pd: 639 | desc = pd['description'] 640 | print(f"- {pd['key']} ({pd['jsonType']}{alias}{rw}): {title}") 641 | if desc: 642 | print(f" Description: {desc}") 643 | if pd['key'] in self.wp.allProps.keys(): 644 | print( 645 | f" Value: {mqtt_get_encoded_property(pd,value)}{' (raw:' + utils_value2json(value) + ')' if 'valueMap' in pd else ''}") 646 | else: 647 | print( 648 | f" NOTE: This property is currently not provided by the connected device!") 649 | 650 | def _watched_event_received(self, event, *args): 651 | print(f"Event of type '{event['type']}' with args '{args}' received!") 652 | 653 | def _watched_property_changed(self, wp, name, value): 654 | if name in self.watching_properties: 655 | pd = self.wpdef["properties"][name] 656 | _LOGGER.info( 657 | f"Property {name} changed to {mqtt_get_encoded_property(pd,value)}") 658 | 659 | def _watched_message_received(self, event, message): 660 | msg_dict = json.loads(message) 661 | if msg_dict["type"] in self.watching_messages: 662 | _LOGGER.info(f"Message of type {msg_dict['type']} received: {message}") 663 | 664 | def _ensure_connected(self): 665 | if not self.wp or not self.wp._connected: 666 | print('Not connected to wattpilot!') 667 | return False 668 | return True 669 | 670 | def _get_props_matching_regex(self, arg, available_only=True): 671 | args = arg.split(' ') 672 | prop_regex = '.*' 673 | if len(args) > 0 and args[0] != '': 674 | prop_regex = args[0] 675 | props = {k: v for k, v in wp_get_all_props(available_only).items() if re.match( 676 | r'^'+prop_regex+'$', k, flags=re.IGNORECASE)} 677 | value_regex = '.*' 678 | if len(args) > 1: 679 | value_regex = args[1] 680 | props = {k: v for k, v in props.items() if re.match(r'^'+value_regex+'$', 681 | str(mqtt_get_encoded_property(self.wpdef["properties"][k], v)), flags=re.IGNORECASE)} 682 | return props 683 | 684 | 685 | #### MQTT Functions #### 686 | 687 | def mqtt_get_mapped_value(pd, value): 688 | mapped_value = value 689 | if value == None: 690 | mapped_value = None 691 | elif "valueMap" in pd: 692 | if str(value) in list(pd["valueMap"].keys()): 693 | mapped_value = pd["valueMap"][str(value)] 694 | else: 695 | _LOGGER.warning( 696 | f"Unable to map value '{value}' of property '{pd['key']} - using unmapped value!") 697 | return mapped_value 698 | 699 | 700 | def mqtt_get_mapped_property(pd, value): 701 | if value and "jsonType" in pd and pd["jsonType"] == "array": 702 | mapped_value = [] 703 | for v in value: 704 | mapped_value.append(mqtt_get_mapped_value(pd, v)) 705 | else: 706 | mapped_value = mqtt_get_mapped_value(pd, value) 707 | return mapped_value 708 | 709 | 710 | def mqtt_get_remapped_value(pd, mapped_value): 711 | remapped_value = mapped_value 712 | if "valueMap" in pd: 713 | if mapped_value in pd["valueMap"].values(): 714 | remapped_value = json.loads(str(list(pd["valueMap"].keys())[ 715 | list(pd["valueMap"].values()).index(mapped_value)])) 716 | else: 717 | _LOGGER.warning( 718 | f"Unable to remap value '{mapped_value}' of property '{pd['key']} - using mapped value!") 719 | return remapped_value 720 | 721 | 722 | def mqtt_get_remapped_property(pd, mapped_value): 723 | if "jsonType" in pd and pd["jsonType"] == "array": 724 | remapped_value = [] 725 | for v in mapped_value: 726 | remapped_value.append(mqtt_get_remapped_value(pd, v)) 727 | else: 728 | remapped_value = mqtt_get_remapped_value(pd, mapped_value) 729 | return remapped_value 730 | 731 | 732 | def mqtt_get_encoded_property(pd, value): 733 | mapped_value = mqtt_get_mapped_property(pd, value) 734 | if value == None or "jsonType" in pd and ( 735 | pd["jsonType"] == "array" 736 | or pd["jsonType"] == "object" 737 | or pd["jsonType"] == "boolean"): 738 | return json.dumps(mapped_value, cls=JSONNamespaceEncoder) 739 | else: 740 | return mapped_value 741 | 742 | 743 | def mqtt_get_decoded_property(pd, value): 744 | if "jsonType" in pd and (pd["jsonType"] == "array" or pd["jsonType"] == "object"): 745 | decoded_value = json.loads(value) 746 | else: 747 | decoded_value = value 748 | return mqtt_get_remapped_property(pd, decoded_value) 749 | 750 | 751 | def mqtt_publish_property(wp, mqtt_client, pd, value, force_publish=False): 752 | prop_name = pd["key"] 753 | if not (force_publish or not Cfg.MQTT_PROPERTIES.val or prop_name in Cfg.MQTT_PROPERTIES.val): 754 | _LOGGER.debug(f"Skipping publishing of property '{prop_name}' ...") 755 | return 756 | property_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_STATE.val, { 757 | "baseTopic": Cfg.MQTT_TOPIC_BASE.val, 758 | "serialNumber": wp.serial, 759 | "propName": prop_name, 760 | }) 761 | encoded_value = mqtt_get_encoded_property(pd, value) 762 | _LOGGER.debug( 763 | f"Publishing property '{prop_name}' with value '{encoded_value}' to MQTT ...") 764 | mqtt_client.publish(property_topic, encoded_value, retain=True) 765 | if Cfg.WATTPILOT_SPLIT_PROPERTIES.val and "childProps" in pd: 766 | _LOGGER.debug( 767 | f"Splitting child props of property {prop_name} as {pd['jsonType']} for value {value} ...") 768 | for cpd in pd["childProps"]: 769 | _LOGGER.debug(f"Extracting child property {cpd['key']}, ...") 770 | split_value = wp_get_child_prop_value(cpd['key']) 771 | _LOGGER.debug( 772 | f"Publishing sub-property {cpd['key']} with value {split_value} to MQTT ...") 773 | mqtt_publish_property(wp, mqtt_client, cpd, split_value, True) 774 | 775 | 776 | def mqtt_publish_message(event, message): 777 | _LOGGER.debug(f"""mqtt_publish_message(event={event},message={message})""") 778 | global mqtt_client 779 | global wpdef 780 | wp = event['wp'] 781 | if mqtt_client == None: 782 | _LOGGER.debug(f"Skipping MQTT message publishing.") 783 | return 784 | msg_dict = json.loads(message) 785 | if Cfg.MQTT_PUBLISH_MESSAGES.val and (not Cfg.MQTT_MESSAGES.val or msg_dict["type"] in Cfg.MQTT_MESSAGES.val): 786 | message_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_MESSAGES.val, { 787 | "baseTopic": Cfg.MQTT_TOPIC_BASE.val, 788 | "serialNumber": wp.serial, 789 | "messageType": msg_dict["type"], 790 | }) 791 | mqtt_client.publish(message_topic, message) 792 | if Cfg.MQTT_PUBLISH_PROPERTIES.val and msg_dict["type"] in ["fullStatus", "deltaStatus"]: 793 | for prop_name, value in msg_dict["status"].items(): 794 | pd = wpdef["properties"][prop_name] 795 | mqtt_publish_property(wp, mqtt_client, pd, value) 796 | 797 | # Substitute topic patterns 798 | 799 | 800 | def mqtt_subst_topic(s, values, expand=True): 801 | if expand: 802 | s = re.sub(r'^~', Cfg.MQTT_TOPIC_PROPERTY_BASE.val, s) 803 | all_values = { 804 | "baseTopic": Cfg.MQTT_TOPIC_BASE.val, 805 | } | values 806 | return s.format(**all_values) 807 | 808 | 809 | def mqtt_setup_client(host, port, client_id, available_topic, command_topic, username="", password=""): 810 | # Connect to MQTT server: 811 | mqtt_client = mqtt.Client(client_id) 812 | mqtt_client.on_message = mqtt_set_value 813 | _LOGGER.info(f"Connecting to MQTT host {host} on port {port} ...") 814 | mqtt_client.will_set( 815 | available_topic, payload="offline", qos=0, retain=True) 816 | if username != "": 817 | mqtt_client.username_pw_set(username, password) 818 | mqtt_client.connect(host, port) 819 | mqtt_client.loop_start() 820 | mqtt_client.publish(available_topic, payload="online", qos=0, retain=True) 821 | _LOGGER.info(f"Subscribing to command topics {command_topic}") 822 | mqtt_client.subscribe(command_topic) 823 | return mqtt_client 824 | 825 | 826 | def mqtt_setup(wp): 827 | _LOGGER.debug(f"mqtt_setup(wp)") 828 | 829 | # Connect to MQTT server: 830 | mqtt_client = mqtt_setup_client(Cfg.MQTT_HOST.val, Cfg.MQTT_PORT.val, Cfg.MQTT_CLIENT_ID.val, 831 | mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}), 832 | mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_SET.val, { 833 | "propName": "+"}), 834 | Cfg.MQTT_USERNAME.val, 835 | Cfg.MQTT_PASSWORD.val, 836 | ) 837 | Cfg.MQTT_PROPERTIES.val = mqtt_get_watched_properties(wp) 838 | _LOGGER.info( 839 | f"Registering message callback to publish updates to the following properties to MQTT: {Cfg.MQTT_PROPERTIES.val}") 840 | wp.add_event_handler(wattpilot.Event.WS_MESSAGE, mqtt_publish_message) 841 | return mqtt_client 842 | 843 | 844 | def mqtt_stop(mqtt_client): 845 | if mqtt_client.is_connected(): 846 | _LOGGER.info(f"Disconnecting from MQTT server ...") 847 | mqtt_client.disconnect() 848 | 849 | # Subscribe to topic for setting property values: 850 | 851 | 852 | def mqtt_set_value(client, userdata, message): 853 | global wpdef 854 | topic_regex = mqtt_subst_topic( 855 | Cfg.MQTT_TOPIC_PROPERTY_SET.val, {"propName": "([^/]+)"}) 856 | name = re.sub(topic_regex, r'\1', message.topic) 857 | if not name or name == "" or not wpdef["properties"][name]: 858 | _LOGGER.warning(f"Unknown property '{name}'!") 859 | pd = wpdef["properties"][name] 860 | if pd['rw'] == "R": 861 | _LOGGER.warning(f"Property '{name}' is not writable!") 862 | try: 863 | value = int(mqtt_get_decoded_property(pd, str(message.payload.decode("utf-8")))) 864 | except ValueError: 865 | value = mqtt_get_decoded_property(pd, str(message.payload.decode("utf-8"))) 866 | _LOGGER.info( 867 | f"MQTT Message received: topic={message.topic}, name={name}, value={value}") 868 | wp.send_update(name, value) 869 | 870 | 871 | def mqtt_get_watched_properties(wp): 872 | if not Cfg.MQTT_PROPERTIES.val: 873 | return list(wp.allProps.keys()) 874 | else: 875 | return Cfg.MQTT_PROPERTIES.val 876 | 877 | 878 | #### Home Assistant Functions #### 879 | 880 | # Generate device information for HA discovery 881 | def ha_get_device_info(wp): 882 | ha_device = { 883 | "connections": [ 884 | ], 885 | "identifiers": [ 886 | f"wattpilot_{wp.serial}", 887 | ], 888 | "manufacturer": wp.manufacturer, 889 | "model": wp.devicetype, 890 | "name": wp.name, 891 | "suggested_area": "Garage", 892 | "sw_version": wp.version, 893 | } 894 | if "maca" in wp.allProps: 895 | ha_device["connections"] += [["mac", wp.allProps["maca"]]] 896 | if "macs" in wp.allProps: 897 | ha_device["connections"] += [["mac", wp.allProps["macs"]]] 898 | return ha_device 899 | 900 | 901 | def ha_get_component_for_prop(prop_info): 902 | component = "sensor" 903 | if "rw" in prop_info and prop_info["rw"] == "R/W": 904 | if "valueMap" in prop_info: 905 | component = "select" 906 | elif "jsonType" in prop_info and prop_info["jsonType"] == "boolean": 907 | component = "switch" 908 | elif "jsonType" in prop_info and prop_info["jsonType"] == "float": 909 | component = "number" 910 | elif "jsonType" in prop_info and prop_info["jsonType"] == "integer": 911 | component = "number" 912 | elif "rw" in prop_info and prop_info["rw"] == "R": 913 | if "jsonType" in prop_info and prop_info["jsonType"] == "boolean": 914 | component = "binary_sensor" 915 | return component 916 | 917 | 918 | def ha_get_default_config_for_prop(prop_info): 919 | config = {} 920 | if "rw" in prop_info and prop_info["rw"] == "R/W": 921 | if "jsonType" in prop_info and \ 922 | (prop_info["jsonType"] == "float" or prop_info["jsonType"] == "integer"): 923 | config["mode"] = "box" 924 | if "category" in prop_info and prop_info["category"] == "Config": 925 | config["entity_category"] = "config" 926 | if "homeAssistant" not in prop_info: 927 | config["enabled_by_default"] = False 928 | return config 929 | 930 | 931 | def ha_get_template_filter_from_json_type(json_type): 932 | template = "{{ value | string }}" 933 | if json_type == "float": 934 | template = "{{ value | float }}" 935 | elif json_type == "integer": 936 | template = "{{ value | int }}" 937 | elif json_type == "boolean": 938 | template = "{{ value == 'true' }}" 939 | return template 940 | 941 | # Publish HA discovery config for a single property 942 | 943 | 944 | def ha_discover_property(wp, mqtt_client, pd, disable_discovery=False, force_enablement=None): 945 | name = pd["key"] 946 | ha_info = {} 947 | if "homeAssistant" in pd: 948 | ha_info = pd["homeAssistant"] or {} 949 | component = ha_get_component_for_prop(pd) 950 | if "component" in ha_info: # Override component from config 951 | component = ha_info["component"] 952 | _LOGGER.debug( 953 | f"Homeassistant config: haInfo={ha_info}, component={component}") 954 | title = pd.get("title", pd.get("alias", name)) 955 | _LOGGER.debug( 956 | f"Publishing HA discovery config for property '{name}' ...") 957 | ha_config = ha_info.get("config", {}) 958 | unique_id = f"wattpilot_{wp.serial}_{name}" 959 | object_id = f"wattpilot_{name}" 960 | topic_subst_map = { 961 | "component": component, 962 | "propName": name, 963 | "serialNumber": wp.serial, 964 | "uniqueId": unique_id, 965 | } 966 | ha_device = ha_get_device_info(wp) 967 | base_topic = mqtt_subst_topic( 968 | Cfg.MQTT_TOPIC_PROPERTY_BASE.val, topic_subst_map, False) 969 | ha_discovery_config = ha_get_default_config_for_prop(pd) | { 970 | "~": base_topic, 971 | "name": title, 972 | "object_id": object_id, 973 | "unique_id": unique_id, 974 | "state_topic": mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_STATE.val, topic_subst_map, False), 975 | "availability_topic": mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}), 976 | "payload_available": "online", 977 | "payload_not_available": "offline", 978 | "device": ha_device, 979 | } 980 | if "valueMap" in pd: 981 | ha_discovery_config["options"] = list(pd["valueMap"].values()) 982 | if pd.get("rw", "") == "R/W": 983 | ha_discovery_config["command_topic"] = mqtt_subst_topic( 984 | Cfg.MQTT_TOPIC_PROPERTY_SET.val, topic_subst_map, False) 985 | ha_discovery_config = dict( 986 | list(ha_discovery_config.items()) 987 | + list(ha_config.items()) 988 | ) 989 | if force_enablement != None: 990 | ha_discovery_config["enabled_by_default"] = force_enablement 991 | topic_cfg = mqtt_subst_topic(Cfg.HA_TOPIC_CONFIG.val, topic_subst_map) 992 | if disable_discovery: 993 | payload = '' 994 | else: 995 | payload = utils_value2json(ha_discovery_config) 996 | _LOGGER.debug( 997 | f"Publishing property '{name}' to {topic_cfg}: {payload}") 998 | mqtt_client.publish(topic_cfg, payload, retain=True) 999 | # Publish additional read-only sensor for special rw properties: 1000 | if pd.get("rw", "") == "R/W" and component != "sensor": 1001 | if payload != "": 1002 | del ha_discovery_config["command_topic"] 1003 | payload = utils_value2json(ha_discovery_config) 1004 | mqtt_client.publish(mqtt_subst_topic(Cfg.HA_TOPIC_CONFIG.val, topic_subst_map | { 1005 | "component": "sensor"}), payload, retain=True) 1006 | if Cfg.WATTPILOT_SPLIT_PROPERTIES.val and "childProps" in pd: 1007 | for p in pd["childProps"]: 1008 | ha_discover_property(wp, mqtt_client, p, 1009 | disable_discovery, force_enablement) 1010 | 1011 | 1012 | def ha_is_default_prop(pd): 1013 | v = "homeAssistant" in pd 1014 | if not Cfg.HA_DISABLED_ENTITIES.val: 1015 | ha = pd.get("homeAssistant", {}) if pd.get("homeAssistant", {}) else {} 1016 | v = v and ha.get("config", {}).get("enabled_by_default", True) 1017 | return v 1018 | 1019 | 1020 | def ha_get_discovery_properties(): 1021 | global wpdef 1022 | _LOGGER.debug( 1023 | f"get_ha_discovery_properties(): HA_PROPERTIES='{Cfg.HA_PROPERTIES.val}', propdef size='{len(wpdef['properties'])}'") 1024 | ha_properties = Cfg.HA_PROPERTIES.val 1025 | if ha_properties == [''] or ha_properties == []: 1026 | ha_properties = [p["key"] 1027 | for p in wpdef["properties"].values() if ha_is_default_prop(p)] 1028 | _LOGGER.debug( 1029 | f"get_ha_discovery_properties(): ha_properties='{ha_properties}'") 1030 | return ha_properties 1031 | 1032 | 1033 | def ha_discover_properties(mqtt_client, ha_properties, disable_discovery=True): 1034 | global wpdef 1035 | _LOGGER.info( 1036 | f"{'Disabling' if disable_discovery else 'Enabling'} HA discovery for the following properties: {ha_properties}") 1037 | for name in ha_properties: 1038 | ha_discover_property( 1039 | wp, mqtt_client, wpdef["properties"][name], disable_discovery) 1040 | 1041 | 1042 | def ha_publish_initial_properties(wp, mqtt_client): 1043 | global wpdef 1044 | _LOGGER.info( 1045 | f"Publishing all initial property values to MQTT to populate the entity values ...") 1046 | for prop_name in Cfg.HA_PROPERTIES.val: 1047 | if prop_name in wp.allProps: 1048 | value = wp.allProps[prop_name] 1049 | pd = wpdef["properties"][prop_name] 1050 | mqtt_publish_property(wp, mqtt_client, pd, value) 1051 | 1052 | 1053 | def ha_setup(wp): 1054 | global wpdef 1055 | # Configure list of relevant properties: 1056 | Cfg.HA_PROPERTIES.val = ha_get_discovery_properties() 1057 | if Cfg.MQTT_PROPERTIES.val == [] or Cfg.MQTT_PROPERTIES.val == ['']: 1058 | Cfg.MQTT_PROPERTIES.val = Cfg.HA_PROPERTIES.val 1059 | # Setup MQTT client: 1060 | mqtt_client = mqtt_setup(wp) 1061 | # Publish HA discovery config: 1062 | ha_discover_properties(mqtt_client, Cfg.HA_PROPERTIES.val, False) 1063 | # Wait a bit for HA to catch up: 1064 | wait_time = math.ceil( 1065 | Cfg.HA_WAIT_INIT_S.val + len(Cfg.HA_PROPERTIES.val)*Cfg.HA_WAIT_PROPS_MS.val*0.001) 1066 | if wait_time > 0: 1067 | _LOGGER.info( 1068 | f"Waiting {wait_time}s to allow Home Assistant to discovery entities and subscribe MQTT topics before publishing initial values ...") 1069 | # Sleep to let HA discover the entities before publishing values 1070 | sleep(wait_time) 1071 | # Publish initial property values to MQTT: 1072 | ha_publish_initial_properties(wp, mqtt_client) 1073 | return mqtt_client 1074 | 1075 | 1076 | def ha_stop(mqtt_client): 1077 | ha_discover_properties(mqtt_client, Cfg.HA_PROPERTIES.val, True) 1078 | mqtt_stop(mqtt_client) 1079 | 1080 | class Env(): 1081 | def __init__(self, datatype, default, description, name="", val="", required=False, requiredIf=None): 1082 | self.datatype = datatype 1083 | self.default = default 1084 | self.description = description 1085 | self.name = name 1086 | self.val = val 1087 | self.required = required 1088 | self.requiredIf = requiredIf 1089 | def format(self): 1090 | val = self.val 1091 | if self.datatype == "password" and self.val != "": 1092 | val = "********" 1093 | return f"{self.name}={val}" 1094 | 1095 | 1096 | # Wattpilot Configuration 1097 | class Cfg(Enum): 1098 | HA_DISABLED_ENTITIES = Env("boolean", "false", "Create disabled entities in Home Assistant") 1099 | HA_ENABLED = Env("boolean", "false", "Enable Home Assistant Discovery") 1100 | HA_PROPERTIES = Env("list", "", "List of space-separated properties that should be discovered by Home Assistant (leave unset for all properties having `homeAssistant` set in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml)") 1101 | HA_TOPIC_CONFIG = Env("string", "homeassistant/{component}/{uniqueId}/config", "Topic pattern for HA discovery config") 1102 | HA_WAIT_INIT_S = Env("integer", "0", "Wait initial number of seconds after starting discovery (in addition to wait time depending on the number of properties). May be increased, if entities in HA are not populated with values.") 1103 | HA_WAIT_PROPS_MS = Env("integer", "0", "Wait milliseconds per property after discovery before publishing property values. May be increased, if entities in HA are not populated with values.") 1104 | MQTT_AVAILABLE_PAYLOAD = Env("string", "online", "Payload for the availability topic in case the MQTT bridge is online") 1105 | MQTT_CLIENT_ID = Env("string", "wattpilot2mqtt", "MQTT client ID") 1106 | MQTT_ENABLED = Env("boolean", "false", "Enable MQTT") 1107 | MQTT_HOST = Env("string", "", "MQTT host to connect to", requiredIf='MQTT_ENABLED') 1108 | MQTT_MESSAGES = Env("list", "", "List of space-separated message types to be published to MQTT (leave unset for all messages)") 1109 | MQTT_NOT_AVAILABLE_PAYLOAD = Env("string", "offline", "Payload for the availability topic in case the MQTT bridge is offline (last will message)") 1110 | MQTT_PASSWORD = Env("password", "", "Password for connecting to MQTT") 1111 | MQTT_PORT = Env("integer", "1883", "Port of the MQTT host to connect to") 1112 | MQTT_PROPERTIES = Env("list", "", "List of space-separated property names to publish changes for (leave unset for all properties)") 1113 | MQTT_PUBLISH_MESSAGES = Env("boolean", "false", "Publish received Wattpilot messages to MQTT") 1114 | MQTT_PUBLISH_PROPERTIES = Env("boolean", "true", "Publish received property values to MQTT") 1115 | MQTT_TOPIC_AVAILABLE = Env("string", "{baseTopic}/available", "Topic pattern to publish Wattpilot availability status to") 1116 | MQTT_TOPIC_BASE = Env("string", "wattpilot", "Base topic for MQTT") 1117 | MQTT_TOPIC_MESSAGES = Env("string", "{baseTopic}/messages/{messageType}", "Topic pattern to publish Wattpilot messages to") 1118 | MQTT_TOPIC_PROPERTY_BASE = Env("string", "{baseTopic}/properties/{propName}", "Base topic for properties") 1119 | MQTT_TOPIC_PROPERTY_SET = Env("string", "~/set", "Topic pattern to listen for property value changes for") 1120 | MQTT_TOPIC_PROPERTY_STATE = Env("string", "~/state", "Topic pattern to publish property values to") 1121 | MQTT_USERNAME = Env("string", "", "Username for connecting to MQTT") 1122 | WATTPILOT_AUTOCONNECT = Env("boolean", "true", "Automatically connect to Wattpilot on startup") 1123 | WATTPILOT_AUTO_RECONNECT = Env("boolean", "true", "Automatically re-connect to Wattpilot on lost connections") 1124 | WATTPILOT_CONNECT_TIMEOUT = Env("integer", "30", "Connect timeout for Wattpilot connection") 1125 | WATTPILOT_HOST = Env("string", "", "IP address of the Wattpilot device to connect to", required=True) 1126 | WATTPILOT_INIT_TIMEOUT = Env("integer", "30", "Wait timeout for property initialization") 1127 | WATTPILOT_LOGLEVEL = Env("string", "INFO", "Log level (CRITICAL,ERROR,WARNING,INFO,DEBUG)") 1128 | WATTPILOT_PASSWORD = Env("password", "", "Password for connecting to the Wattpilot device", required=True) 1129 | WATTPILOT_RECONNECT_INTERVAL = Env("integer", "30", "Waiting time in seconds before a lost connection is re-connected") 1130 | WATTPILOT_SPLIT_PROPERTIES = Env("boolean", "true", "Whether compound properties (e.g. JSON arrays or objects) should be decomposed into separate properties") 1131 | 1132 | @classmethod 1133 | def set(cls, env: dict): 1134 | for var in list(cls): 1135 | #print(f"Setting parameter {var.name} ...") 1136 | d = var.value 1137 | d.name = var.name 1138 | strval = env.get(var.name, d.default) 1139 | if d.datatype == "boolean": 1140 | d.val = (strval == "true") 1141 | elif d.datatype == "integer": 1142 | d.val = int(strval) 1143 | elif d.datatype == "list": 1144 | d.val = strval.split(sep=' ') if strval else [] 1145 | elif d.datatype == "password": 1146 | d.val = strval 1147 | if strval != "": 1148 | strval = "********" 1149 | elif d.datatype == "string": 1150 | d.val = strval 1151 | _LOGGER.debug(f"{d.format()} (from '{strval}')") 1152 | assert not d.required or d.val, f"{var.name} is not set!" 1153 | for var in [e for e in list(cls) if e.value.requiredIf]: 1154 | d = var.value 1155 | assert not Cfg[d.requiredIf].value.val or d.val, f"{var.name} is not set (required for '{d.requiredIf}')!" 1156 | 1157 | @classmethod 1158 | def docs_markdown(cls): 1159 | print("|" + "|".join(["Environment Variable", "Type", "Default Value", "Description"]) + "|") 1160 | print("|" + "|".join(["--------------------", "----", "-------------", "-----------"]) + "|") 1161 | for e in list(cls): 1162 | d = e.value 1163 | print("|" + "|".join([f"`{e.name}`", f"`{d.datatype}`", f"{'`'+d.default+'`' if d.default else ''}", d.description]) + "|") 1164 | 1165 | @property 1166 | def val(self): 1167 | return self.value.val 1168 | 1169 | @val.setter 1170 | def val(self,value): 1171 | self.value.val = value 1172 | 1173 | 1174 | #### Main Program #### 1175 | 1176 | def main(): 1177 | global mqtt_client 1178 | global wp 1179 | global wpdef 1180 | 1181 | # Set debug level: 1182 | logging.basicConfig(level=os.environ.get('WATTPILOT_LOGLEVEL','INFO').upper()) 1183 | 1184 | # Setup environment variables: 1185 | Cfg.set(os.environ) 1186 | 1187 | # Initialize globals: 1188 | mqtt_client = None 1189 | wp = wp_initialize(Cfg.WATTPILOT_HOST.val, Cfg.WATTPILOT_PASSWORD.val) 1190 | wpdef = wp_read_apidef() # TODO: Should be part of the wattpilot core library! 1191 | 1192 | # Initialize shell: 1193 | wpsh = WattpilotShell(wp, wpdef) 1194 | if Cfg.WATTPILOT_AUTOCONNECT.val: 1195 | _LOGGER.info("Automatically connecting to Wattpilot ...") 1196 | wpsh.do_connect("") 1197 | # Enable MQTT and/or HA integration: 1198 | if Cfg.MQTT_ENABLED.val and not Cfg.HA_ENABLED.val: 1199 | wpsh.do_mqtt("start") 1200 | elif Cfg.MQTT_ENABLED.val and Cfg.HA_ENABLED.val: 1201 | wpsh.do_ha("start") 1202 | wpsh.do_info("") 1203 | if len(sys.argv) < 2: 1204 | wpsh.cmdloop() 1205 | else: 1206 | wpsh.onecmd(sys.argv[1]) 1207 | 1208 | 1209 | if __name__ == '__main__': 1210 | try: 1211 | main() 1212 | except KeyboardInterrupt: 1213 | try: 1214 | sys.exit(0) 1215 | except SystemExit: 1216 | os._exit(0) 1217 | -------------------------------------------------------------------------------- /test-shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script helps during development to test certain aspects of the wattpilot shell. 4 | 5 | if [ -f dev.env ]; then 6 | source dev.env 7 | fi 8 | 9 | export PYTHONPATH=src 10 | 11 | WPCONFIG_FILE="src/wattpilot/ressources/wattpilot.yaml" 12 | 13 | cmd="${1:-default}" 14 | shift 1 15 | 16 | function runShell() { 17 | python -m wattpilot.wattpilotshell "${@}" 18 | } 19 | 20 | function runShellOnly() { 21 | WATTPILOT_AUTOCONNECT=false MQTT_ENABLED=false HA_ENABLED=false WATTPILOT_LOGLEVEL=WARN runShell "${@}" 22 | } 23 | 24 | function runShellWithProps() { 25 | PROPS="${1:-}" 26 | echo "Enabled properties: ${PROPS}" 27 | MQTT_ENABLED=true HA_ENABLED=true MQTT_PROPERTIES="${PROPS}" HA_PROPERTIES="${PROPS}" runShell 28 | } 29 | 30 | function runShellWithAllProps() { 31 | PROP_FILTER="${1:-.*}" 32 | PROPS=$(gojq --yaml-input -r -c '.properties[] | (.key, .childProps?[]?.key?)' "${WPCONFIG_FILE}" | grep "^${PROP_FILTER}" | xargs echo) 33 | runShellWithProps "${PROPS}" 34 | } 35 | 36 | 37 | case "${cmd}" in 38 | default) 39 | runShell "${@}" 40 | ;; 41 | shell-only) 42 | runShellOnly "${@}" 43 | ;; 44 | server) 45 | WATTPILOT_AUTOCONNECT=true MQTT_ENABLED=true HA_ENABLED=true runShell "server" 46 | ;; 47 | save) 48 | logfile=work/status-$(date +"%Y-%m-%d_%H-%M-%S")-${1:-adhoc}.log 49 | mkdir -p work 50 | WATTPILOT_LOGLEVEL=WARNING MQTT_ENABLED=false HA_ENABLED=false runShell "values" >>${logfile} 2>&1 51 | WATTPILOT_LOGLEVEL=WARNING MQTT_ENABLED=false HA_ENABLED=false runShell "rawvalues" >>${logfile} 2>&1 52 | WATTPILOT_LOGLEVEL=WARNING MQTT_ENABLED=false HA_ENABLED=false runShell "properties" >>${logfile} 2>&1 53 | ;; 54 | ha) 55 | runShellWithProps 56 | ;; 57 | ha-all) 58 | runShellWithAllProps 59 | ;; 60 | ha-test-props) 61 | runShellWithProps "alw loe nrg fhz spl3 acu ama cdi ffna fna" 62 | ;; 63 | ha-test-array) 64 | runShellWithProps "nrg" 65 | ;; 66 | ha-test-boolean) 67 | runShellWithProps "alw loe" 68 | ;; 69 | ha-test-float) 70 | runShellWithProps "fhz spl3" 71 | ;; 72 | ha-test-integer) 73 | runShellWithProps "acu ama" 74 | ;; 75 | ha-test-object) 76 | runShellWithProps "cdi" 77 | ;; 78 | ha-test-string) 79 | runShellWithProps "ffna fna" 80 | ;; 81 | update-docs) 82 | python gen-apidocs.py >API.md 83 | ( 84 | echo "# Wattpilot Shell Commands" 85 | for cmd in $( 86 | runShellOnly "help" \ 87 | | grep -v -E '^(===+|.*:)$' \ 88 | | xargs -n 1 echo \ 89 | | grep -E -v '^EOF$' \ 90 | | sort \ 91 | ); do 92 | echo "" 93 | echo "## ${cmd}" 94 | echo "" 95 | echo "\`\`\`bash" 96 | runShellOnly "help ${cmd}" 97 | echo "\`\`\`" 98 | done 99 | ) >ShellCommands.md 100 | ( 101 | echo "# Wattpilot Shell Environment Variables" 102 | echo "" 103 | runShellOnly docs 104 | ) >ShellEnvVariables.md 105 | ;; 106 | validate-yaml) 107 | echo -n "Properties with missing mandatory fields: " 108 | gojq --yaml-input -c '[.properties[],.childProps[]? | select(.key==null or .jsonType==null)]' <"${WPCONFIG_FILE}" 109 | echo -n "Child properties with missing jsonType: " 110 | gojq --yaml-input -c '[.properties[] | . as $ppd | .childProps[]? | . as $cpd | select(.jsonType==null and $ppd.itemType==null) | .key]' <"${WPCONFIG_FILE}" 111 | ;; 112 | *) 113 | echo "Unknown command: ${cmd}" 114 | exit 1 115 | ;; 116 | esac 117 | --------------------------------------------------------------------------------