├── .github └── workflows │ ├── autoblack.yml │ ├── docker+pypi.yml │ ├── gitleaks.yml │ └── test_suite.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LOCAL-CONTROL.md ├── MANIFEST.in ├── Makefile ├── README.md ├── TWCManager.py ├── contrib ├── .twcmanager.service.testing ├── docker │ ├── Dockerfile │ ├── docker-compose-v1.2.2.yml │ ├── docker-compose-v1.2.3.yml │ ├── docker-compose-v1.2.4.yml │ ├── docker-compose.yml │ └── entrypoint.sh └── twcmanager.service ├── docs ├── Debug.md ├── DevelopmentGuide.md ├── Features.md ├── Gen3_Status.md ├── InstallationGuide.md ├── PolicyCustomization.md ├── README.md ├── Scheduling.md ├── Settings.md ├── SlaveProtocol.md ├── Software_Docker.md ├── Software_Manual.md ├── TWCManager Installation.pdf ├── TeslaAPI_Rediection.md ├── Troubleshooting.md ├── charge_curve.png ├── config_examples.md ├── interface.jpg ├── modules │ ├── Control_HTTP.md │ ├── Control_MQTT.md │ ├── Control_WebIPC.md │ ├── EMS_DSMRreader.md │ ├── EMS_Efergy.md │ ├── EMS_EmonCMS.md │ ├── EMS_Enphase.md │ ├── EMS_Fronius.md │ ├── EMS_Growatt.md │ ├── EMS_HASS.md │ ├── EMS_Iotawatt.md │ ├── EMS_Kostal.md │ ├── EMS_OpenHab.md │ ├── EMS_OpenWeatherMap.md │ ├── EMS_P1Monitor.md │ ├── EMS_Powerwall2.md │ ├── EMS_SmartMe.md │ ├── EMS_SmartPi.md │ ├── EMS_SolarEdge.md │ ├── EMS_SolarLog.md │ ├── EMS_TED.md │ ├── EMS_URL.md │ ├── EMS_Volkszahler.md │ ├── Interface_Dummy.md │ ├── Interface_RS485.md │ ├── Interface_TCP.md │ ├── Logging_CSV.md │ ├── Logging_Console.md │ ├── Logging_Files.md │ ├── Logging_MySQL.md │ ├── Logging_SQLite.md │ ├── Logging_Sentry.md │ ├── Status_HASS.md │ ├── Status_MQTT.md │ ├── Vehicle_FleetTelemetryMQTT.md │ ├── Vehicle_TeslaBLE.md │ ├── Vehicle_TeslaMate.md │ └── control_HTTP_API │ │ ├── addConsumptionOffset.md │ │ ├── cancelChargeNow.md │ │ ├── chargeNow.md │ │ ├── deleteConsumptionOffset.md │ │ ├── getConsumptionOffsets.md │ │ ├── saveSettings.md │ │ ├── sendStartCommand.md │ │ ├── sendStopCommand.md │ │ ├── setScheduledChargingSettings.md │ │ └── setSetting.md ├── pyenv.md ├── rotary-switch.png ├── screenshot.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── torxscrews.jpg ├── twccover.png └── twcinternalcover.png ├── etc └── twcmanager │ ├── .testconfig.json │ ├── config.json │ └── demo.json ├── html ├── favicon.png ├── index.php └── refresh.png ├── lib └── TWCManager │ ├── .gitignore │ ├── Control │ ├── HTTPControl.py │ ├── MQTTControl.py │ ├── WebIPCControl.py │ ├── static │ │ ├── css │ │ │ └── styles.css │ │ └── js │ │ │ ├── commonUI.js │ │ │ ├── debug.js │ │ │ ├── settings.js │ │ │ └── status.js │ └── themes │ │ ├── Default │ │ ├── bootstrap.html.j2 │ │ ├── debug.html.j2 │ │ ├── drawChart.html.j2 │ │ ├── graphs.html.j2 │ │ ├── handle_teslalogin.html.j2 │ │ ├── jsrefresh.html.j2 │ │ ├── main.html.j2 │ │ ├── navbar.html.j2 │ │ ├── policy.html.j2 │ │ ├── request_teslalogin.html.j2 │ │ ├── schedule.html.j2 │ │ ├── settings.html.j2 │ │ ├── showCommands.html.j2 │ │ ├── showStatus.html.j2 │ │ ├── upgrade.html.j2 │ │ ├── upgradePrompt.html.j2 │ │ ├── vehicleDetail.html.j2 │ │ └── vehicles.html.j2 │ │ └── Modern │ │ ├── bootstrap.js.html.j2 │ │ ├── css │ │ └── styles.css │ │ ├── jsrefresh.html.j2 │ │ ├── main.html.j2 │ │ └── master.html.j2 │ ├── EMS │ ├── .gitignore │ ├── DSMR.py │ ├── DSMRreader.py │ ├── Efergy.py │ ├── EmonCMS.py │ ├── Enphase.py │ ├── Fronius.py │ ├── Growatt.py │ ├── HASS.py │ ├── IotaWatt.py │ ├── Kostal.py │ ├── MQTT.py │ ├── OpenHab.py │ ├── OpenWeatherMap.py │ ├── P1Monitor.py │ ├── SmartMe.py │ ├── SmartPi.py │ ├── SolarEdge.py │ ├── SolarLog.py │ ├── TED.py │ ├── TeslaPowerwall2.py │ ├── URL.py │ └── Volkszahler.py │ ├── Interface │ ├── Dummy.py │ ├── RS485.py │ └── TCP.py │ ├── Logging │ ├── CSVLogging.py │ ├── ConsoleLogging.py │ ├── FileLogging.py │ ├── MySQLLogging.py │ ├── SQLiteLogging.py │ └── SentryLogging.py │ ├── Policy │ └── Policy.py │ ├── Protocol │ └── TWCProtocol.py │ ├── Status │ ├── .gitignore │ ├── HASSStatus.py │ └── MQTTStatus.py │ ├── TWCManager.py │ ├── TWCMaster.py │ ├── TWCSlave.py │ └── Vehicle │ ├── FleetTelemetryMQTT.py │ ├── Telemetry.py │ ├── TeslaAPI.py │ ├── TeslaBLE.py │ └── TeslaMateVehicle.py ├── requirements.txt ├── setup.py └── tests ├── API ├── test_apilistener.sh ├── test_chargeNow.py ├── test_consumptionOffsets.py ├── test_getActivePolicyAction.py ├── test_getConfig.py ├── test_getLastTWCResponse.py ├── test_getPolicy.py ├── test_getSlaveTWCs.py ├── test_getStatus.py ├── test_sendStartCommand.py ├── test_sendStopCommand.py └── test_setLatLon.py ├── EMS ├── ems_emulator.py ├── test_MQTT.py └── test_SmartPi.py ├── Makefile ├── pre-flight ├── check_environment.py └── env_preparation.py └── scripts ├── mysql_setup.py ├── sqlite_setup.py └── upload_file.sh /.github/workflows/autoblack.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action that uses Black to reformat the Python code in an incoming pull request. 2 | # If all Python code in the pull request is compliant with Black then this Action does nothing. 3 | # Othewrwise, Black is run and its changes are committed back to the incoming pull request. 4 | # https://github.com/cclauss/autoblack 5 | 6 | name: Automatically format sources 7 | 8 | defaults: 9 | run: 10 | working-directory: ./black 11 | 12 | on: 13 | push: 14 | branches: 15 | - 'ci_dev' 16 | - 'main' 17 | 18 | jobs: 19 | build: 20 | name: Format Sources 21 | # Temporary while self-hosted infra is down 22 | # runs-on: [ "self-hosted", "build_host" ] 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | path: ./black 28 | ref: ${{ github.event.push.head.sha }} 29 | - name: Set up Python 3.12 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: 3.12 33 | cache: 'pip' 34 | - name: Install Black 35 | run: pip install black 36 | - name: Check if formatting is required for any file 37 | run: black --exclude="setup.py|tests/*" --check . 38 | - name: If needed, commit black changes to the repo 39 | if: failure() 40 | run: | 41 | black --exclude="setup.py|tests/*" . 42 | git config --global user.name 'Auto Format' 43 | git config --global user.email 'ngardiner@gmail.com' 44 | git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY 45 | git checkout 46 | git commit -am "fixup: Format Python code with Black" 47 | git push 48 | -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | name: "Scan with Gitleaks" 2 | on: [pull_request, push, workflow_dispatch] 3 | jobs: 4 | scan: 5 | name: gitleaks 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | fetch-depth: 0 11 | - uses: gitleaks/gitleaks-action@v2 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | GITLEAKS_NOTIFY_USER_LIST: "@ngardiner" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | .vscode/ 4 | dist/ 5 | TWCManager.egg-info/ 6 | .idea/ 7 | venv/ 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /LOCAL-CONTROL.md: -------------------------------------------------------------------------------- 1 | Local Vehicle Control 2 | ===================== 3 | 4 | This is a temporary readme for local control of Tesla Vehicles. 5 | 6 | The situation at the moment is that while this functionality exists, it is very difficult to produce a mature interface, as: 7 | 8 | * Most python BLE modules do not cleanly install on RPi, for example: 9 | * SimplePyBLE doesn't have a wheel for arm6l and doesn't compile cleanly 10 | * bleak requires some build, and does build successfully, but has a long-running bug which means it cannot pair with a Tesla 11 | * python3-bluez is an option but is unsupported going forward 12 | * pyteslable looks useful, but breaks with recent versions of the cryptography module due to the use of 4-byte nonces and requires manual editing of sources to fix that, plus requires SimplePyLBE. 13 | * Even the golang version distributed in some raspbian distributions is lower than the minimum required (1.20) for support with the tesla-control binary, although 1.20 will still fail due to a different issue and 1.21+ is required. We use 1.23 which is what Tesla tested it with. 14 | 15 | Honest Assessment of Current State 16 | ================================== 17 | 18 | * Pairing works, but there isn't currently positive feedback that confirms peering, might need to be achieved through pinging vehicles 19 | 20 | * Should co-exist fine with non-BLE installs. 21 | 22 | * BLE control is generally stable, but temperamental. I have observed that after a period of time, use of BLE control can contribute to walk up unlock not working or requiring bluetooth to be turned off/on on phone, there are also reports of potential instability if there is significant distance between the TWCManager device and the vehicle. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft lib/TWCManager/Control/static 2 | graft lib/TWCManager/Control/themes 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DEPS := git libffi-dev libpq-dev libssl-dev 2 | WEBDEPS := $(DEPS) lighttpd 3 | ARCH := $(shell uname -m) 4 | GOARCH := $(shell echo $(ARCH) | sed s/x86_64/amd64/ | sed s/aarch64/arm64/ | sed s/armv7l/armv6l/) 5 | GODIST := go1.23.4.linux-$(GOARCH).tar.gz 6 | HOME := /home/twcmanager 7 | SUDO := sudo 8 | USER := twcmanager 9 | GROUP := twcmanager 10 | VER := $(shell lsb_release -sr) 11 | BLUETOOTH = $(shell grep -c bluetooth /etc/group) 12 | 13 | .PHONY: tests upload 14 | 15 | build: deps build_pkg 16 | docker: deps build_pkg config tesla-control 17 | webbuild: webdeps build_pkg 18 | 19 | arch: 20 | echo $(ARCH) 21 | config: 22 | # Create twcmanager user and group 23 | $(SUDO) useradd -U -m $(USER) 2>/dev/null; exit 0 24 | $(SUDO) usermod -a -G dialout $(USER) 25 | ifeq ($(BLUETOOTH),1) 26 | $(SUDO) usermod -a -G bluetooth $(USER) 27 | endif 28 | # Create configuration directory 29 | $(SUDO) mkdir -p /etc/twcmanager 30 | ifeq (,$(wildcard /etc/twcmanager/config.json)) 31 | $(SUDO) cp etc/twcmanager/config.json /etc/twcmanager/ 32 | endif 33 | $(SUDO) chown $(USER):$(GROUP) /etc/twcmanager -R 34 | $(SUDO) chmod 755 /etc/twcmanager -R 35 | 36 | deps: 37 | $(SUDO) apt-get update 38 | $(SUDO) apt-get install -y $(DEPS) 39 | 40 | webdeps: 41 | $(SUDO) apt-get update 42 | 43 | ifeq ($(VER), 9.11) 44 | $(SUDO) apt-get install -y $(WEBDEPS) php7.0-cgi 45 | else ifeq ($(VER), stretch) 46 | $(SUDO) apt-get install -y $(WEBDEPS) php7.0-cgi 47 | else ifeq ($(VER), 16.04) 48 | $(SUDO) apt-get install -y $(WEBDEPS) php7.0-cgi 49 | else ifeq ($(VER), 16.10) 50 | $(SUDO) apt-get install -y $(WEBDEPS) php7.0-cgi 51 | else ifeq ($(VER), 20.04) 52 | $(SUDO) apt-get install -y $(WEBDEPS) php7.4-cgi 53 | else 54 | $(SUDO) apt-get install -y $(WEBDEPS) php7.3-cgi 55 | endif 56 | $(SUDO) lighty-enable-mod fastcgi-php ; exit 0 57 | $(SUDO) service lighttpd force-reload ; exit 0 58 | 59 | install: deps install_pkg config 60 | webinstall: webdeps install_pkg config webfiles 61 | 62 | tesla-control: 63 | mkdir -p $(HOME)/gobin 64 | cd $(HOME) && wget https://go.dev/dl/$(GODIST) 65 | cd $(HOME) && tar -xvf $(GODIST) 66 | rm $(HOME)/$(GODIST) 67 | echo "export GOPATH=$(HOME)/go" >> $(HOME)/.bashrc 68 | echo "export $$PATH:\$GOPATH/bin" >> $(HOME)/.bashrc 69 | git clone https://github.com/teslamotors/vehicle-command $(HOME)/vehicle-control || exit 0 70 | cd $(HOME)/vehicle-control && GOPATH=$(HOME)/go PATH=$(HOME)/go/bin:$$PATH go get ./... 71 | cd $(HOME)/vehicle-control && GOPATH=$(HOME)/go PATH=$(HOME)/go/bin:$$PATH go build ./... 72 | cd $(HOME)/vehicle-control && GOPATH=$(HOME)/go PATH=$(HOME)/go/bin:$$PATH GOBIN=$(HOME)/gobin go install ./... 73 | sudo setcap 'cap_net_raw,cap_net_admin+eip' $(HOME)/gobin/tesla-control 74 | 75 | testconfig: 76 | # Create twcmanager user and group 77 | $(SUDO) useradd -U -M $(USER); exit 0 78 | 79 | # Create configuration directory 80 | $(SUDO) mkdir -p /etc/twcmanager 81 | ifeq (,$(wildcard /etc/twcmanager/config.json)) 82 | $(SUDO) cp etc/twcmanager/.testconfig.json /etc/twcmanager/config.json 83 | endif 84 | $(SUDO) chown $(USER):$(GROUP) /etc/twcmanager -R 85 | $(SUDO) chmod 755 /etc/twcmanager -R 86 | 87 | build_pkg: 88 | # Install build pre-requisite 89 | $(SUDO) apt-get -y install python3-venv 90 | 91 | # Install TWCManager packages 92 | ifeq ($(CI), 1) 93 | $(SUDO) /home/docker/.pyenv/shims/pip3 install -r requirements.txt 94 | $(SUDO) /home/docker/.pyenv/shims/python3 -m build 95 | else 96 | ifneq (,$(wildcard /usr/bin/pip3)) 97 | $(SUDO) pip3 install --upgrade pip 98 | $(SUDO) pip3 install --upgrade setuptools 99 | $(SUDO) pip3 install -r requirements.txt 100 | else 101 | ifneq (,$(wildcard /usr/bin/pip)) 102 | $(SUDO) pip install --upgrade pip 103 | $(SUDO) pip install --upgrade setuptools 104 | $(SUDO) pip install -r requirements.txt 105 | endif 106 | endif 107 | $(SUDO) python3 -m build 108 | endif 109 | 110 | install_pkg: 111 | ifneq (,$(wildcard /usr/bin/pip3)) 112 | $(SUDO) pip3 install -r requirements.txt 113 | $(SUDO) pip3 install . 114 | else 115 | ifneq (,$(wildcard /usr/bin/pip)) 116 | $(SUDO) pip install -r requirements.txt 117 | $(SUDO) pip install . 118 | endif 119 | endif 120 | 121 | test_direct: 122 | cd tests && make test_direct 123 | 124 | test_service: 125 | cd tests && make test_service 126 | 127 | test_service_nofail: 128 | cd tests && make test_service_nofail 129 | 130 | tests: 131 | cd tests && make 132 | 133 | upload: 134 | cd tests && make upload 135 | 136 | webfiles: 137 | $(SUDO) cp html/* /var/www/html/ 138 | $(SUDO) chown -R www-data:www-data /var/www/html 139 | $(SUDO) chmod -R 755 /var/www/html 140 | $(SUDO) usermod -a -G www-data $(USER) 141 | -------------------------------------------------------------------------------- /TWCManager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import grp 5 | import pwd 6 | import sys 7 | 8 | # If we are being run as root, drop privileges to twcmanager user 9 | # This avoids any potential permissions issues if it is run as root and settings.json is created as root 10 | if os.getuid() == 0: 11 | user = "twcmanager" 12 | groups = [g.gr_gid for g in grp.getgrall() if user in g.gr_mem] 13 | 14 | _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) 15 | groups.append(gid) 16 | os.setgroups(groups) 17 | os.setgid(gid) 18 | os.setuid(uid) 19 | 20 | sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + "/lib") 21 | 22 | # Remove any local local path references in sys.path, otherwise we'll 23 | # see an error when we try to import TWCManager.TWCManager, as it will see 24 | # us (TWCManager.py) instead of the package (lib/TWCManager) and fail. 25 | if "" in sys.path: 26 | sys.path.remove("") 27 | 28 | if "." in sys.path: 29 | sys.path.remove(".") 30 | 31 | if os.path.dirname(os.path.realpath(__file__)) in sys.path: 32 | sys.path.remove(os.path.dirname(os.path.realpath(__file__))) 33 | 34 | import TWCManager.TWCManager 35 | -------------------------------------------------------------------------------- /contrib/.twcmanager.service.testing: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tesla Wall Charger Manager 3 | 4 | [Service] 5 | Type=simple 6 | User=twcmanager 7 | WorkingDirectory=/tmp 8 | ExecStart=/home/docker/.pyenv/shims/python3 -u -m TWCManager.TWCManager > /tmp/twcmanager-tests/twcmanager.svc.stdout.log 2> /tmp/twcmanager-tests/twcmanager.svc.stderr.log 9 | StandardOutput=file:/tmp/twcmanager-tests/twcmanager.service.stdout.log 10 | StandardError=file:/tmp/twcmanager-tests/twcmanager.service.stderr.log 11 | Restart=always 12 | StartLimitInterval=60s 13 | StartLimitBurst=5 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /contrib/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | RUN apt-get -y update && apt-get -y install build-essential ca-certificates gfortran git libatlas-base-dev libcap2-bin lsb-release make python3 python3-pip util-linux wget 3 | RUN cd /usr/src && git clone https://github.com/ngardiner/TWCManager 4 | 5 | # Install TWCManager 6 | RUN cd /usr/src/TWCManager && make docker SUDO="" 7 | 8 | VOLUME /etc/twcmanager 9 | WORKDIR /usr/src/TWCManager 10 | 11 | ENTRYPOINT ["./contrib/docker/entrypoint.sh"] 12 | CMD ["/usr/bin/python3","-m","TWCManager"] 13 | 14 | # SSL_CERTIFICATE_FAILED errors 15 | # These errors began appearing and impacing the build pipeline in Jul 2021 16 | # They occur only for the arm7 arch (which is the RPi) and only for some 17 | # packages. Affected packages seem to be those with no wheel package for 18 | # arm7. 19 | # 20 | # Things we've investigated: 21 | # - Checked commits around the time it broke, nothing relevant 22 | # - Public or private worker, no change 23 | # - Changed debian to ubuntu LTS, no cbange 24 | # - Skipped installation of cryptography package 25 | # - Skipped impacted packages from setuptools script 26 | -------------------------------------------------------------------------------- /contrib/docker/docker-compose-v1.2.2.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | twcmanager: 5 | image: twcmanager/twcmanager:v1.2.2 6 | restart: always 7 | devices: 8 | - "/dev/ttyUSB0:/dev/ttyUSB0" 9 | ports: 10 | - 80:80 11 | - 8080:8080 12 | # environment: 13 | # - TZ=Australia/Sydney 14 | volumes: 15 | - /etc/twcmanager:/etc/twcmanager 16 | - /etc/localtime:/etc/localtime:ro 17 | -------------------------------------------------------------------------------- /contrib/docker/docker-compose-v1.2.3.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | twcmanager: 5 | image: twcmanager/twcmanager:v1.2.3 6 | restart: always 7 | devices: 8 | - "/dev/ttyUSB0:/dev/ttyUSB0" 9 | ports: 10 | - 80:80 11 | - 8080:8080 12 | # environment: 13 | # - TZ=Australia/Sydney 14 | volumes: 15 | - /etc/twcmanager:/etc/twcmanager 16 | - /etc/localtime:/etc/localtime:ro 17 | -------------------------------------------------------------------------------- /contrib/docker/docker-compose-v1.2.4.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | twcmanager: 5 | image: twcmanager/twcmanager:v1.2.4 6 | restart: always 7 | devices: 8 | - "/dev/ttyUSB0:/dev/ttyUSB0" 9 | ports: 10 | - 80:80 11 | - 8080:8080 12 | # environment: 13 | # - TZ=Australia/Sydney 14 | volumes: 15 | - /etc/twcmanager:/etc/twcmanager 16 | - /etc/localtime:/etc/localtime:ro 17 | -------------------------------------------------------------------------------- /contrib/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | twcmanager: 5 | image: twcmanager/twcmanager:latest 6 | restart: always 7 | devices: 8 | - "/dev/bus/usb:/dev/bus/usb" 9 | - "/dev/ttyUSB0:/dev/ttyUSB0" 10 | ports: 11 | - 80:80 12 | - 8080:8080 13 | environment: 14 | # - TZ=Australia/Sydney 15 | - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket 16 | volumes: 17 | - /etc/twcmanager:/etc/twcmanager 18 | - /etc/localtime:/etc/localtime:ro 19 | - /proc/1/ns/:/rootns 20 | - /run/dbus:/run/dbus:ro 21 | # Note: I recommend removing the following if there's no chance that 22 | # you will need bluetooth (BLE) access 23 | cap_add: 24 | - CAP_NET_ADMIN # Required for managing network interfaces (Bluetooth uses these) 25 | - CAP_NET_RAW # Required for raw network access (Bluetooth uses this) 26 | - CAP_SYS_ADMIN # Needed for Bluetooth HCI access 27 | -------------------------------------------------------------------------------- /contrib/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Entrypoint script for Docker image of TWCManager. 4 | # We use this to prepare the configuration for the first time 5 | if [ ! -e "/etc/twcmanager/config.json" ]; then 6 | cp /usr/src/TWCManager/etc/twcmanager/config.json /etc/twcmanager/config.json 7 | chown twcmanager:twcmanager /etc/twcmanager /etc/twcmanager/config.json 8 | fi 9 | 10 | # This will exec the CMD from your Dockerfile 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /contrib/twcmanager.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tesla Wall Charger Manager 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=twcmanager 8 | WorkingDirectory=/tmp 9 | ExecStart=/usr/bin/python3 -u -m TWCManager.TWCManager 10 | Restart=always 11 | StartLimitInterval=60s 12 | StartLimitBurst=5 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /docs/Debug.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | The Debug Interface provides advanced control over TWCs managed by TWCManager. In normal operation, it is rare to require the use of these advanced interfaces, however if you have a use case that isn't covered by the TWCManager defaults, you may choose to make modifications using the Debug interface. 4 | 5 | ## Sending Debug Commands 6 | 7 | The Send Debug Commands function is a powerful interface to allow direct querying of TWCs using commands that provide visibility of internal settings and values within the TWC. 8 | 9 | ### Warning 10 | 11 | There are several known commands which do cause damage to TWCs! These are blocked by the TWCManager Debug Interface and will result in a warning being printed and the command being ignored, however there may be other commands that we are not aware of that could damage your TWC, and as a result you should limit your commands to known commands. 12 | 13 | ## Advanced Settings 14 | ### Spike Amps 15 | 16 | The Spike Amps option in advanced settings allows you to configure when the TWCManager will spike the power offered to the vehicle in order to ensure that the vehicle charges at the highest offered rate wherever possible. 17 | 18 | There are a number of scenarios in which it has been identified that Tesla vehicles may require this spike in order to react to the change in power offering. 19 | 20 | There are two options that may be tuned: 21 | 22 | * Proactively 23 | 24 | TWCManager will proactively spike the amps offered to TWCs where the amps offered are not greater than a certain value. This avoids situations where vehicles may get "stuck" on the lower power offering. 25 | 26 | * Reactively 27 | 28 | Reactive amp spiking is where a vehicle is detected as being "stuck" on a given offering. If we have increased our offering and the vehicle has not responded for some time, a spike in the offered amps is used to "reset" the vehicle's charge rate. 29 | -------------------------------------------------------------------------------- /docs/DevelopmentGuide.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | ## Introduction 4 | 5 | Welcome to TWCManager development, and a very big thank you for your contribution to the project. This guide is intended to kick-start development efforts by acting as a knowledgebase for useful details about developing for TWCManager. 6 | 7 | ## Testing & Developing 8 | 9 | The easiest way to maintan and test a TWCManager source tree is to run the TWCManager.py script directly from the repository directory. For example: 10 | 11 | ### Cloning Git Repository 12 | 13 | ``` 14 | git clone https://github.com/ngardiner/TWCManager 15 | cd TWCManager 16 | ``` 17 | 18 | ### Make changes 19 | 20 | This is your working branch of the repository. 21 | 22 | ### Running the script locally 23 | 24 | ``` 25 | ./TWCManager.py 26 | ``` 27 | 28 | ### Adding new python dependencies 29 | 30 | Python dependencies are documented in the setup.py script. 31 | 32 | ## Conventions 33 | 34 | ### Debug Levels 35 | 36 | Currently, there are inconsistent debug levels used throughout the project. This has been flagged as a high priority area of improvement. The following tables aim to clarify 37 | 38 | #### Core (TWCManager, TWCMaster, TWCSlave) 39 | 40 | | Debug Level | Used for | 41 | | ----------- | -------- | 42 | | 1 | Notification to users, initialization messages and errors | 43 | | 1 | Confirmation of policy selection | 44 | | 2 | Internal error/parameter issue eg missing value for *internal* function call | 45 | | 7 | Policy selection, module loaded | 46 | | 8 | Policy parameter comparison and non-selection of policy | 47 | | 10 | Loop entry, loop exit debugs | 48 | | 11 | Developer-defined debug checkpoints/output/etc | 49 | 50 | #### Modules (Control, EMS, Status) 51 | 52 | | Debug Level | Used for | 53 | | ----------- | -------- | 54 | | 1 | Critical error which prevents module functionality (eg. not configured / incorrect config | 55 | | 10 | Loop entry, loop exit debugs | 56 | | 11 | Developer-defined debug checkpoints/output/etc | 57 | 58 | ### When working with persistent values (config/settings) 59 | 60 | The values which are stored in the config and settings dicts are interpreted from JSON storage after each restart. This can cause an issue, in that whilst they are a true representation of the data 61 | -------------------------------------------------------------------------------- /docs/Features.md: -------------------------------------------------------------------------------- 1 | # TWCManager Features 2 | 3 | ## Introduction 4 | 5 | If you're new to the TWCManager project, or if you're not new but want to know what we've been busy working on, here's a long list of features that TWCManager provides today 6 | 7 | ## Vehicle Support 8 | 9 | * Full support for Tesla vehicles including the ability to Start/Stop charging via API, and to recognize vehicle VINs. 10 | * Ability to track vehicles which have used Sub-TWC devices to charge, and to specify whether the vehicle has permissions to charge 11 | * Limited support for other vehicles through generic interactions 12 | 13 | ## Green Power Support 14 | 15 | * Support for a large array of Solar Inverters, allowing tracking of Generation and Consumption values (where supported) and the ability to charge your vehicle using the delta between the Generated and Consumed power, essentially charging your vehicle for free from the sun 16 | * Support for a number of Battery systems including Growatt and Tesla Powerwall, where charging can be controlled via appropriate SOC, Generation and Consumption values. 17 | * Integration with other projects including openWB for control of charging 18 | * Unable to find a module which works for your inverter or have an inverter which 19 | 20 | ## Control of TWC Devices 21 | 22 | * Support for controlling your TWC devices through a number of interfaces including an in-built Web UI, RESTful API, HomeAssistant and MQTT interfaces. 23 | 24 | ## Technical Features 25 | 26 | * Tested and compatible across a wide range of Python versions, with full support for apt-based distributions (Debian, Ubuntu, Raspberry Pi OS) and with Docker packages for ease of deployment. 27 | -------------------------------------------------------------------------------- /docs/Scheduling.md: -------------------------------------------------------------------------------- 1 | # Charge Scheduling 2 | 3 | ## What's changing? 4 | 5 | In v1.2.1, we (finally) introduce a new Charge Scheduling UI, and with it a few new Charge Scheduling features. 6 | 7 | Unfortunately, these features are not directly backwards compatible. For this reason, the approach involves taking 3 key steps towards a migration to the new scheduling functions: 8 | 9 | * Step 1: Introduce UI and gather feedback, with previous functionality level available 10 | 11 | * In this stage, features such as setting Charge Times per Day, and scheduling charges down to the minute are exposed in the new UI and can be configured, but will be written in a backwards-compatible format as well as a new format, with only the current level of Charge Scheduling support (single schedule across multiple days) being implemented. 12 | 13 | * This allows configuration of Charge Schedule using the new Web UI, but also allows us to structure the new UI in a way that takes advantage of new features. It will still be compatible with the old Web UI, and allows configuration of scheduling with a partial feature set from the new Web UI. 14 | 15 | * Step 2: Migration of existing Charge Schedule settings to new configuration structure 16 | 17 | * Step 2 will occur sometime after the initial Step 1 implementation, but will only occur in instances where the new UI has not been used - existing deployments. This will migrate current settings to the new structure on a once-off basis 18 | 19 | * Step 3: Deprecation of old interface 20 | 21 | * Step 3 involves the Charge Policy code being switched from the old configuration parameters to the new configuration parameters, and the enabling of new functionality. At this point, the legacy Web UI will no longer be capable of configuring Charge Scheduling. 22 | 23 | -------------------------------------------------------------------------------- /docs/SlaveProtocol.md: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Overview of protocol TWCs use to load share 3 | # 4 | # A TWC set to slave mode (rotary switch position F) sends a linkready message 5 | # every 10 seconds. 6 | # The message contains a unique 4-byte id that identifies that particular slave 7 | # as the sender of the message. 8 | # 9 | # A TWC set to master mode sees a linkready message. In response, it sends a 10 | # heartbeat message containing the slave's 4-byte id as the intended recipient 11 | # of the message. 12 | # The master's 4-byte id is included as the sender of the message. 13 | # 14 | # Slave sees a heartbeat message from master directed to its unique 4-byte id 15 | # and responds with its own heartbeat message containing the master's 4-byte id 16 | # as the intended recipient of the message. 17 | # The slave's 4-byte id is included as the sender of the message. 18 | # 19 | # Master sends a heartbeat to a slave around once per second and expects a 20 | # response heartbeat from the slave. 21 | # Slaves do not send heartbeats without seeing one from a master first. If 22 | # heartbeats stop coming from master, slave resumes sending linkready every 10 23 | # seconds. 24 | # If slaves stop replying to heartbeats from master, master stops sending 25 | # heartbeats after about 26 seconds. 26 | # 27 | # Heartbeat messages contain a data block used to negotiate the amount of power 28 | # available to each slave and to the master. 29 | # The first byte is a status indicating things like is TWC plugged in, does it 30 | # want power, is there an error, etc. 31 | # Next two bytes indicate the amount of power requested or the amount allowed in 32 | # 0.01 amp increments. 33 | # Next two bytes indicate the amount of power being used to charge the car, also in 34 | # 0.01 amp increments. 35 | # Remaining bytes always contain a value of 0. 36 | -------------------------------------------------------------------------------- /docs/TWCManager Installation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/TWCManager Installation.pdf -------------------------------------------------------------------------------- /docs/TeslaAPI_Rediection.md: -------------------------------------------------------------------------------- 1 | # Tesla API DNS Redirection Trick 2 | 3 | ## Introduction 4 | 5 | In September 2021, Tesla (once again) changed the API login flow. In the past, we've adopted the Tesla login flow, however this time they added an element to the flow that prohibits us from doing this. 6 | 7 | They have added an origin check to the login flow. This origin check has the effect of requiring that the Capcha Check be run from a tesla.com domain. 8 | 9 | ## Workaround 10 | 11 | To work around this requirement, you need to make a change on your side (or use a different method). 12 | 13 | The change involves a DNS host file entry (or appropriate entry on your router or DNS resolver if you're able) which points a subdomain of tesla.com, such as twcmanager.tesla.com at your TWCManager instance. 14 | 15 | If you do this, and access TWCManager with this URL (eg http://twcmanager.tesla.com:8080), you will be able to log in using your Tesla credentials and completing a Recaptcha challenge. If your setup is compatible, you'll see a Congratulations message and see the Captcha prompt. 16 | 17 | ### Hosts file entry 18 | 19 | The hosts fie entry should look like this: 20 | 21 | ``` 22 | 192.168.1.1 twcmanager.tesla.com 23 | ``` 24 | 25 | The hosts file is located at ```c:\windows\system32\drivers\etc\hosts``` 26 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## First Steps 4 | 5 | ### Debug Level 6 | 7 | Before undertaking any troubleshooting, the number one most useful piece of informaton to have on hand is a lot with the debugLevel turned up high. 8 | 9 | To make sure you capture everything, set debugLevel to 11 in ```/etc/twcmanager/config.json``` and run TWCManager in the following way to log the output: 10 | 11 | ```python -u -m TWCManager | tee debug.log``` 12 | 13 | Please provide the log output when you [raise an issue on GitHub](https://github.com/ngardiner/TWCManager/issues/)! This information is most useful in diagnosing a problem. 14 | 15 | ### TWCManager Version 16 | 17 | Are you using the development version of TWCManager? If so, and if you are having issues, please switch to the Stable version and see if it is working. If so, you've found a bug! Please [raise an issue on GitHub](https://github.com/ngardiner/TWCManager/issues/) and let us know. 18 | 19 | ## Adapter 20 | 21 | * The required adaptor for this communication is an **RS485** adaptor. Be careful that you are not using an RS232 adaptor which is more common, but which uses a different duplexing system and uses more pins to communicate. 22 | 23 | * If you did happen to use an RS232 adaptor, you may decode the communications correctly, but you will likely have issues with transmissions (due to the lack of RTS/CTS signalling and half-duplex operation) 24 | 25 | * Known working adapters include: 26 | 27 | * USB-RS485-WE-1800-BT 28 | * JBtek USB to RS485 Converter Adapter ch340T 29 | * DSD TECH USB 2.0 to RS485 Serial Data Converter CP2102 30 | * Raspberry Pi RS422 RS485 Shield 31 | 32 | ## Wiring 33 | 34 | * There are two In and two Out pins for RS485 Master/Slave configuration on a V2 TWC. The important detail is the polarity (+/-) of each wire, it does not effectively matter which of the 2 pairs of pins you use for TWCManager communications. 35 | 36 | * The following diagram gives a good overview of the Half-Duplex system used for TWC communication: https://zone.ni.com/reference/en-XX/help/373197L-01/lvaddon11/987x_halfduplex/ 37 | 38 | * In some cases, messages can become corrupted unless a 120 ohm resistor is placed in parallel between the TX and RX lines. The following diagram (see the Half-Duplex diagram) provides a good overview of this: https://zone.ni.com/reference/en-XX/help/373197L-01/lvaddon11/987x_rs485termination/ 39 | 40 | * Similarly, some installations have seen corruption unless 680 ohm "bias" resistors are wired between the D+ (usually orange) wire and the Red (+5v) wire, and the D- (usually yellow) wire and Black (Gnd) wire. 41 | 42 | * You should twist the pair of wires around each other to avoid cross-talk. As short as 6 inches of non-twisted wire is enough to cause cross-talk corruption. In addition, you should avoid long cable runs wherever possible. 43 | 44 | * Check your terminals to ensure they are tightly screwed or wound. 45 | 46 | ## Messages 47 | 48 | The TWCManager communications protocol consists of messages sent back and forward between Master and Slave. Observing these messages will assist to identify issues with your configuration. 49 | 50 | ### Slave Linkready 51 | 52 | The Slave linkready message is: 53 | 54 | ```fd e2 .. .. .. .. .. 00 00 00 00 00 00``` 55 | 56 | (The .. sections being the Slave ID, Sign and Maximum Amps) 57 | 58 | This is sent when: 59 | 60 | * The slave is first powered on and is looking to link to a master 61 | * The master stopped communicating with the slave for 30 seconds. 62 | 63 | If you are seeing repeated Slave Linkready messages (x amp slave TWC is ready to link) in the logs with your debugLevel set to 1 or more, this signals that an issue with communications is stopping your TWCManager from managing the Slave TWC. 64 | 65 | ### Unknown Message Type 66 | 67 | When a message is recieved which does not match the currently known message types, TWCManager will log ```*** UNKNOWN MESSAGE FROM SLAVE:``` 68 | 69 | These messages are particularly important when debugging communication, as they either signal: 70 | 71 | * A new message from a Slave that doesn't match previously known protocol signatures was recieved (unlikely), or 72 | * There is some issue with the wiring between the TWCs that causes the messages to be recieved in a corrupted form, or to run into one another. 73 | 74 | ### Checksum incorrect 75 | 76 | If you see Checksum does not match messages, it means the checksum bit is not correct when computed from the data sent. This is a very strong sign of corruption between TWCManager and the Slave TWC(s). 77 | 78 | The recommendation here is to refer to the cabling section above and ensure everything is per the recommendations there. 79 | 80 | ## LED Lights 81 | 82 | The LED lights on the TWC are useful for debugging what is happening. 83 | 84 | * Continuous blinking red light on the TWC suggests that it is in slave mode and has not been in communication with a master for 30 seconds or more. 85 | -------------------------------------------------------------------------------- /docs/charge_curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/charge_curve.png -------------------------------------------------------------------------------- /docs/interface.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/interface.jpg -------------------------------------------------------------------------------- /docs/modules/Control_HTTP.md: -------------------------------------------------------------------------------- 1 | # HTTP Control Module 2 | 3 | ## Introduction 4 | 5 | The HTTP Control module allows control of the TWCManager Tesla Wall Charger controller via an in-built HTTP web server. 6 | 7 | The web-server is multi-threaded (ie, it can be managed by multiple clients simultaneously), but does not support HTTPS encryption. It listens on Port 8080. As of release v1.1.5, it does not currently have any configurable options (but will in the future). 8 | 9 | ### HTTP Control Module vs IPC Web Interface 10 | 11 | There are two separate interfaces for managing TWCManager via web browser. These are: 12 | 13 | * WebIPC - The original web interface bundled with TWCManager 14 | * HTTPControl - The new in-built web interface 15 | 16 | **Benefits of HTTPControl** 17 | 18 | * Tightly integrated with the TWCManager controller. Less development lead-time to add functions. 19 | 20 | **Drawbacks of HTTPControl** 21 | 22 | * Does not support HTTPS encryption. 23 | 24 | ### Status 25 | 26 | | Detail | Value | 27 | | --------------- | -------------- | 28 | | **Module Name** | HTTPControl | 29 | | **Module Type** | Status | 30 | | **Status** | In Development | 31 | 32 | ## Configuration 33 | 34 | The following table shows the available configuration parameters for the MQTT Control module. 35 | 36 | | Parameter | Value | 37 | | ----------- | ------------- | 38 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will enable HTTP control. | 39 | | listenPort | *optional* HTTP Web Server port. Defaults to port 8080. | 40 | 41 | ### JSON Configuration Example 42 | 43 | ``` 44 | "control": { 45 | "HTTP": { 46 | "enabled": true, 47 | "listenPort": 8080 48 | } 49 | } 50 | ``` 51 | 52 | ## Using the HTTP Web Interface 53 | 54 | If you have enabled HTTPControl, access it via the specified port. For example if your TWCManager machine is 192.168.1.1 and listenPort is 8080, access the HTTP interface with the following URL: 55 | 56 | http://192.168.1.1:8080/ 57 | 58 | ## Using the API Interface 59 | 60 | The HTTPControl web server provides an API interface under the /api URL root. The following methods are used when interacting with the API interface: 61 | 62 | * GET requests for requesting information or parameters 63 | * POST requests for performing actions or providing data 64 | 65 | The following API endpoints exist: 66 | 67 | | Endpoint | Method | Description | 68 | | --------------------------- | ------ | ------------------------------------------------- | 69 | | [addConsumptionOffset](control_HTTP_API/addConsumptionOffset.md) | POST | Add or Edit a Consumption Offset value | 70 | | [cancelChargeNow](control_HTTP_API/cancelChargeNow.md) | POST | Cancels active chargeNow configuration | 71 | | [chargeNow](control_HTTP_API/chargeNow.md) | POST | Instructs charger to start charging at specified rate | 72 | | [deleteConsumptionOffset](control_HTTP_API/deleteConsumptionOffset.md) | POST | Delete a Consumption Offset value | 73 | | getConfig | GET | Provides the current configuration | 74 | | [getConsumptionOffsets](control_HTTP_API/getConsumptionOffsets.md) | GET | List configured offsets | 75 | | getPolicy | GET | Provides the policy configuration | 76 | | getSlaveTWCs | GET | Provides a list of connected Slave TWCs and their state | 77 | | getStatus | GET | Provides the current status (Charge Rate, Policy) | 78 | | getUUID | GET | Provides a unique ID for this particular master, based on the physical MAC address | 79 | | [saveSettings](control_HTTP_API/saveSettings.md) | POST | Saves settings to settings file | 80 | | [sendStartCommand](control_HTTP_API/sendStartCommand.md) | POST | Sends the Start command to all Slave TWCs | 81 | | [setSetting](control_HTTP_API/setSetting.md) | POST | Set settings directly via API | 82 | | [sendStopCommand](control_HTTP_API/sendStopCommand.md) | POST | Sends the Stop command to all Slave TWCs | 83 | | [setScheduledChargingSettings](control_HTTP_API/setScheduledChargingSettings.md) | POST | Saves Scheduled Charging settings --> can be retrieved with getStatus | 84 | -------------------------------------------------------------------------------- /docs/modules/Control_MQTT.md: -------------------------------------------------------------------------------- 1 | # MQTT Control Module 2 | 3 | ## Introduction 4 | 5 | The MQTT Control Module allows control over the TWCManager Tesla Wall Charger controller using MQTT topics. By publishing commands via MQTT, the behaviour of the charger (such as charger timing and mode) can be controlled. 6 | 7 | ### Status 8 | 9 | | Detail | Value | 10 | | --------------- | -------------- | 11 | | **Module Name** | MQTTControl | 12 | | **Module Type** | Status | 13 | | **Status** | In Development | 14 | 15 | ## Configuration 16 | 17 | The following table shows the available configuration parameters for the MQTT Control module. 18 | 19 | | Parameter | Value | 20 | | ----------- | ------------- | 21 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will enable MQTT control. | 22 | | brokerIP | *required* The IP address of the MQTT broker. We subscribe to control topics under this MQTT broker. | 23 | | topicPrefix | *required* MQTT topic prefix for control topics. | 24 | | username | *optional* Username for connecting to MQTT server (if authentication is required). | 25 | | password | *optional* Password for connecting to MQTT broker (if authentication is required). | 26 | 27 | ### JSON Configuration Example 28 | 29 | ``` 30 | "control": { 31 | "MQTT": { 32 | "enabled": true, 33 | "brokerIP": "192.168.1.2", 34 | "topicPrefix": "TWC", 35 | "username": "mqttuser", 36 | "password": "mqttpass" 37 | } 38 | } 39 | ``` 40 | 41 | ## MQTT Topics 42 | 43 | | MQTT Topic | Value | Description | 44 | | ------------------------------ | ----- | ----------- | 45 | | *prefix*/control/chargeNow | AA,SS | Starts charging at AA amps for SS seconds immediately. e.g. `8,3600` = 8 Amps for 1 hour | 46 | | *prefix*/control/chargeNowEnd | None | Stops immediate charging. | 47 | | *prefix*/control/stop | None | Stops TWCManager. | 48 | -------------------------------------------------------------------------------- /docs/modules/Control_WebIPC.md: -------------------------------------------------------------------------------- 1 | # Web IPC Control Module 2 | 3 | ## Introduction 4 | 5 | The Web IPC Control module allows control of the TWCManager Tesla Wall Charger controller via an external HTTP web server, using PHP scripts. This is the web interface that was used in cdragon's TWCManager fork. 6 | 7 | This offers decoupling of the Web Server component from TWCManager. 8 | 9 | ### Note 10 | 11 | In v1.2.1, we disable the use of the WebIPC interface by default. We recommend using the new HTTP Control interface. You may need to use the legacy Web Interface if: 12 | 13 | * You are using any feature that is not available in the new interface (should only be the debug interface at this point) 14 | * You are interfacing with openWB, which uses the old web interface to control charge rate. 15 | 16 | To enable the Web IPC module, configure the following in your ```config.json``` file: 17 | 18 | ``` 19 | "control": { 20 | "IPC": { 21 | "enabled": true 22 | }, 23 | ``` 24 | 25 | ### HTTP Control Module vs IPC Web Interface 26 | 27 | There are two separate interfaces for managing TWCManager via web browser. These are: 28 | 29 | * WebIPC - The original web interface bundled with TWCManager 30 | * HTTPControl - The new in-built web interface 31 | 32 | **Benefits of WebIPCControl** 33 | 34 | * Supports HTTPS (when used with a HTTPS-capable Web Server) 35 | 36 | **Drawbacks of WebIPCControl** 37 | 38 | * More complex - requires additional administration of a web server to operate. 39 | 40 | ### Status 41 | 42 | | Detail | Value | 43 | | --------------- | -------------- | 44 | | **Module Name** | WebIPCControl | 45 | | **Module Type** | Status | 46 | | **Status** | In Development | 47 | -------------------------------------------------------------------------------- /docs/modules/EMS_DSMRreader.md: -------------------------------------------------------------------------------- 1 | # DSMR-reader EMS Module 2 | 3 | ## Introduction 4 | 5 | The [DSMR-reader](https://github.com/dsmrreader/dsmr-reader) EMS module allows fetching of Consumption and Production values from the DSMR-reader JSON MQTT messages. 6 | 7 | ## How it works 8 | 9 | TWCManager will subscribe to the JSON Telegram MQTT topic of DSMR-reader (`dsmr/json` by default, configure this at `/admin/dsmr_mqtt/jsontelegrammqttsettings/` of your DSMR-reader). 10 | 11 | Comsumption is taken from the `electricity_currently_delivered` DSMR-reader value. 12 | 13 | Production is taken from the `electricity_currently_returned` DSMR-reader value. 14 | 15 | ### Dependencies 16 | 17 | DSMR-reader needs to publish the JSON Telegram messages to an MQTT broker. 18 | 19 | ### Note 20 | 21 | Given that DSMR-reader measures the total household consumption, this includes the TWC. As a result, the TWC's load will be included in the Consumption via the P1 output of the smart power meter. The smart meter does not know the total PV power delivery, just how much power is being delivered back to the grid. Please ensure the following configuration settings are enabled in your `config.json` file: 22 | 23 | ``` 24 | { 25 | "config": { 26 | "subtractChargerLoad": true, 27 | "treatGenerationAsGridDelivery": true, 28 | } 29 | } 30 | ``` 31 | 32 | ### Status 33 | 34 | | Detail | Value | 35 | | --------------- | ------------------------------ | 36 | | **Module Name** | DSMRreader | 37 | | **Module Type** | Energy Management System (EMS) | 38 | | **Features** | Consumption, Production | 39 | | **Status** | In Development | 40 | 41 | ## Configuration 42 | 43 | The following table shows the available configuration parameters for the P1 Monitor EMS module. 44 | 45 | | Parameter | Value | 46 | | ----------- | ------------- | 47 | | brokerIP | *required* The IP address of the MQTT broker. | 48 | | brokerPort | *optional* The port of the MQTT broker. | 49 | | username | *optional* The username for the MQTT broker. | 50 | | password | *optional* The password for the MQTT broker. | 51 | | topic | *optional* The MQTT topic where the JSON messages will be published. | 52 | 53 | ### JSON Configuration Example 54 | 55 | ``` 56 | { 57 | "sources":{ 58 | "DSMRreader": { 59 | "brokerIP": "192.168.1.2", 60 | "brokerPort": 1883, 61 | "username": "mqttuser", 62 | "password": "mqttpass", 63 | "topic": "dsmr/json", 64 | } 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/modules/EMS_Efergy.md: -------------------------------------------------------------------------------- 1 | # Efergy 2 | 3 | Documentation is in progress 4 | 5 | ## Configuration 6 | 7 | The following table shows the available configuration parameters for the Efergy EMS module. 8 | 9 | ### Cloud API Configuration 10 | 11 | | Parameter | Value | 12 | | ----------- | ------------- | 13 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Efergy API. | 14 | | token | *required* The Token Code provided to access the API interface. | 15 | 16 | -------------------------------------------------------------------------------- /docs/modules/EMS_EmonCMS.md: -------------------------------------------------------------------------------- 1 | # Open Energy Monitor EMS Module 2 | 3 | ## Introduction 4 | 5 | The Open Energy Monitor (EmonCMS) EMS module allows fetching of solar generation and consumption values from Open Energy Monitor. This is useful as it allows a general interface to data which may be combined within Open Energy Monitor, eg. accumulating generation data from multiple inverters. 6 | 7 | ### Status 8 | 9 | | Detail | Value | 10 | | --------------- | ------------------------------ | 11 | | **Module Name** | EmonCMS | 12 | | **Module Type** | Energy Management System (EMS) | 13 | | **Features** | Consumption, Generation | 14 | | **Status** | Implemented, Tested | 15 | 16 | ## Configuration 17 | 18 | The following table shows the available configuration parameters for the HASS EMS module. 19 | 20 | | Parameter | Value | 21 | | ----------- | ------------- | 22 | | apiKey | *required* API Key. You can find this under "Feed API Help" in your Open Energy Monitor Feeds page. | 23 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll HomeAssistant sensors. | 24 | | consumptionFeed | *optional* The ID of the consumption feed in OpenEnergyMonitor. | 25 | | generationFeed | *optional* The ID of the generation feed in OpenEnergyMonitor. | 26 | | serverIP | *required* The IP address or hostname of the Open Energy Monitor instance. We will poll the REST HTTP API. | 27 | | serverPort | *optional* The port that the webserver for Open Energy Monitor is listening on. Defaults to 80 (HTTP). | 28 | | serverPath | *optional* The HTTP path, if Open Energy Monitor is setup in a subdirectory. Defaults to empty, you should add a trailing '/'. | 29 | | useHttps | *optional* Boolean value, ```true``` or ```false```. Should it use https instead of http. | 30 | 31 | ### JSON Configuration Example 32 | 33 | ``` 34 | "EmonCMS": { 35 | "apiKey": "ABC123", 36 | "enabled": true, 37 | "consumptionFeed": 1, 38 | "generationFeed": 2, 39 | "serverIP": "power.local", 40 | "serverPort": 80 41 | } 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /docs/modules/EMS_Enphase.md: -------------------------------------------------------------------------------- 1 | # Enphase Envoy EMS Module 2 | 3 | ## Introduction 4 | 5 | Enphase Inverters provide either cloud-based or local API access, which allows querying of Solar Generation information. 6 | 7 | This module supports either the querying of the public web-based API, or querying of the local inverter API, depending on the configuration supplied. 8 | 9 | ## Configuration 10 | 11 | The following table shows the available configuration parameters for the Enphase EMS module. 12 | 13 | ### Cloud API Configuration 14 | 15 | | Parameter | Value | 16 | | ----------- | ------------- | 17 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Enphase API. | 18 | | systemID | *required* The System ID allocated to your Enphase Envoy inverter. | 19 | | userID | *required* The User ID allocated to your Enphase Envoy installation. | 20 | 21 | ### Local API Configuration 22 | 23 | | Parameter | Value | 24 | | ----------- | ------------- | 25 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Enphase API. | 26 | | serverIP | *required* The IP address of the Enphase Envoy Inverter. We will poll this device's HTTP API. | 27 | | serverPort | *optional* Defaults to Port 80. | 28 | 29 | 30 | ### JSON Configuration Example 31 | 32 | #### Cloud API 33 | 34 | ``` 35 | "Enphase": { 36 | "enabled": true, 37 | "apiKey": "abcdef", 38 | "systemID": 1234, 39 | "userID": 1234 40 | } 41 | ``` 42 | 43 | #### Local API 44 | 45 | ``` 46 | "Enphase": { 47 | "enabled": true, 48 | "serverIP": "192.168.1.2", 49 | "serverPort": 80 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/modules/EMS_Fronius.md: -------------------------------------------------------------------------------- 1 | # Fronius Inverter EMS Module 2 | 3 | ## Introduction 4 | 5 | Fronius Inverters provide a solar.web interface locally on the inverter itself, which allows querying of Solar Generation information. If you have a Fronius Meter installed, the solar.web interface also provides Consumption information. 6 | 7 | Fronius Inverters connect via wifi. The serverIP IP address is the IP that the Fronius Inverter is provided via DHCP after connecting to the Wifi network. 8 | 9 | ### Note 10 | 11 | In many Fronius installations, the installation will involve a Fronius Meter mounted within the electricity meter box. If you have one of these installed, it will be between 2-4 DIN slots wide, with an LCD screen showing metering information, and will have a model number similar to 63A-1 or 63A-3. 12 | 13 | If you have such a meter installed, you are able to obtain Consumption information via the Fronius interface, and it is likely that the TWC's power draw is being metered. If this is the case, the TWC's load will show as Consumption via the Fronius EMS module. If this is the case, please ensure the following configuration setting is enabled in your ```config.json``` file: 14 | 15 | ``` 16 | { 17 | "config": { 18 | "subtractChargerLoad": true 19 | } 20 | } 21 | ``` 22 | 23 | ### Status 24 | 25 | | Detail | Value | 26 | | --------------- | ------------------------------ | 27 | | **Module Name** | Fronius | 28 | | **Module Type** | Energy Management System (EMS) | 29 | | **Features** | Consumption, Generation | 30 | | **Status** | Implemented, Mature, Tested | 31 | 32 | ## Configuration 33 | 34 | The following table shows the available configuration parameters for the Fronius EMS module. 35 | 36 | | Parameter | Value | 37 | | ----------- | ------------- | 38 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Fronius Inverter. | 39 | | serverIP | *required* The IP address of the Fronius Inverter. We will poll this device's HTTP API. Multiple Fronius inverters can be specified, please see examples below. | 40 | | serverPort | *optional* Web Server port. This is the port that we should connect to. This is almost always 80 (HTTP). | 41 | 42 | ### JSON Configuration Example 43 | 44 | Single inverter configuration: 45 | 46 | ``` 47 | "Fronius": { 48 | "enabled": true, 49 | "serverIP": "192.168.1.2", 50 | "serverPort": 80 51 | } 52 | ``` 53 | 54 | Multiple inverter confgiuration: 55 | 56 | ``` 57 | "Fronius": { 58 | "enabled": true, 59 | "serverIP": [ "192.168.1.2", "192.168.1.3", "192.168.1.4" ], 60 | "serverPort": 80 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/modules/EMS_Growatt.md: -------------------------------------------------------------------------------- 1 | # Growatt 2 | 3 | Documentation is in progress 4 | 5 | ## Configuration 6 | 7 | The following table shows the available configuration parameters for the Growatt EMS module. 8 | 9 | ### Configuration Parameters 10 | 11 | | Parameter | Value | 12 | | ---------------- | ------------- | 13 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Growatt API. | 14 | | batteryMaxOutput | | 15 | | username | *required* The username provided to access the API interface. | 16 | | psssword | | 17 | | useBatteryAt | | 18 | | useBatteryBefore | | 19 | | useBatteryTill | | 20 | -------------------------------------------------------------------------------- /docs/modules/EMS_HASS.md: -------------------------------------------------------------------------------- 1 | # HomeAssistant EMS Module 2 | 3 | ## Introduction 4 | 5 | The HomeAssistant EMS module allows fetching of solar Generation and Consumption values from HomeAssistant sensors. This is useful as it allows a general interface to sensors which are implemented as dedicated HomeAssistant components. If there is no dedicated TWCManager module for an Energy Management System, using the HASS module allows the leveraging of HASS sensors. 6 | 7 | ### Status 8 | 9 | | Detail | Value | 10 | | --------------- | ------------------------------ | 11 | | **Module Name** | HASS | 12 | | **Module Type** | Energy Management System (EMS) | 13 | | **Features** | Consumption, Generation | 14 | | **Status** | Implemented, Mature, Tested | 15 | 16 | ## Configuration 17 | 18 | The following table shows the available configuration parameters for the HASS EMS module. 19 | 20 | | Parameter | Value | 21 | | ----------- | ------------- | 22 | | apiKey | *required* API Key. | 23 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll HomeAssistant sensors. | 24 | | hassEntityConsumption | *optional* Name of HASS Consumption Sensor. | 25 | | hassEntityGeneration | *optional* Name of HASS Generation Sensor. | 26 | | serverIP | *required* The IP address of the HomeAssistant instance. We will poll the REST HTTP API. | 27 | | serverPort | *optional* HASS port. This is the port that we should connect to. Defaults to 8123 (HTTP). | 28 | | useHttps | *optional* Boolean value, ```true``` or ```false```. Should it the call use https instead of http. | 29 | 30 | ### JSON Configuration Example 31 | 32 | ``` 33 | "HASS": { 34 | "apiKey": "ABC123", 35 | "enabled": true, 36 | "hassEntityConsumption": "sensor.consumption", 37 | "hassEntityGeneration": "sensor.generation", 38 | "serverIP": "192.168.1.2", 39 | "serverPort": 8123 40 | } 41 | ``` 42 | 43 | ### Sensor Names 44 | 45 | For HomeAssistant, the two settings below must be customized to point to the specific sensor names you use within HomeAssistant. There is no default or common value for this, so it will require customization to work correctly. 46 | 47 | If you do not track one of these values (generation or consumption) via HASS, leave the parameter blank, and it will not be retrieved. 48 | 49 | ### API Key 50 | 51 | We require a HomeAssistant API Key to provide privileges for access to HomeAssistant sensors. 52 | 53 | To obtain a HASS API key, via browser, click on your user profile, and add a Long-Lived Access Token. 54 | -------------------------------------------------------------------------------- /docs/modules/EMS_Iotawatt.md: -------------------------------------------------------------------------------- 1 | # IoTaWatt EMS Module 2 | 3 | ## Introduction 4 | 5 | The IoTaWatt EMS module allows fetching of solar Generation and Consumption values from Iotawatt outputs. 6 | 7 | ### Status 8 | 9 | | Detail | Value | 10 | | --------------- | ------------------------------ | 11 | | **Module Name** | Iotawatt | 12 | | **Module Type** | Energy Management System (EMS) | 13 | | **Features** | Consumption, Generation | 14 | | **Status** | Available | 15 | 16 | ## Configuration 17 | 18 | The following table shows the available configuration parameters for the IoTaWatt EMS module. 19 | 20 | | Parameter | Value | 21 | | ----------- | ------------- | 22 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the IoTaWatt REST API. | 23 | | outputConsumption | *optional* Name of Consumption output. | 24 | | outputGeneration | *optional* Name of Generation output. | 25 | | serverIP | *required* The IP address of the Iotawatt instance. | 26 | 27 | ### JSON Configuration Example 28 | 29 | ``` 30 | "Iotawatt": { 31 | "enabled": true, 32 | "outputConsumption": "Total_Consumption", 33 | "outputGeneration": "Solar", 34 | "serverIP": "192.168.1.2" 35 | } 36 | ``` 37 | 38 | ### Output Names 39 | 40 | For IoTaWatt, the two outputs must be customized to point to the specific names you use within IoTaWatt. There is no default or common value for this, so it will require customization to work correctly. Note that only outputs (not inputs) can be used for this. 41 | 42 | If you do not track one of these values (generation or consumption) via IoTaWatt, leave the parameter blank, and it will not be retrieved. 43 | -------------------------------------------------------------------------------- /docs/modules/EMS_Kostal.md: -------------------------------------------------------------------------------- 1 | # Kostal EMS Module 2 | 3 | ## Introduction 4 | 5 | The Kostal EMS Module allows energy generation (solar) to be fetched via ModBus TCP, which is a available with all modern Kostal inverters. 6 | 7 | The module supports fetching generation and consumption values directly via ```ModBus TCP``` protocol. 8 | 9 | ## Configuration 10 | 11 | The following table shows the available configuration parameters for the SolarEdge EMS Module: 12 | 13 | | **Parameter** | **Value** | 14 | | ------------- | --------- | 15 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will query values via ModBus TCP | 16 | | serverIP | *required* The IP address of the Kostal inverter in the same LAN (usally something link 192.168.1.x). | 17 | | modbusPort | *required* The port of the ModBus server in the Kostal inverter. The default value (1502) will work in most cases. | 18 | | unitID | *required* The unit ID of the Kostal inverter in the ModBus. Default value is 71 which should work in most cases. | 19 | 20 | Please note, if any of the required parameters for the Kostal EMS module are not specified in the module configuration, the module will not work and unload at start time! 21 | 22 | ## JSON Configuration Example 23 | 24 | ``` 25 | "Kostal": { 26 | "enabled": true, 27 | "serverIP": "192.168.1.100", 28 | "modbusPort": 1502, 29 | "unitID": 71 30 | } 31 | ``` 32 | 33 | 34 | ## Contact 35 | If you have problems with this module, I can provide limited support via hopfi2k@me.com -------------------------------------------------------------------------------- /docs/modules/EMS_OpenHab.md: -------------------------------------------------------------------------------- 1 | # openHAB EMS Module 2 | 3 | ## Introduction 4 | 5 | The openHAB EMS module allows fetching of solar Generation and Consumption values from openHAB items via the openHAB REST API. 6 | 7 | ### Status 8 | 9 | | Detail | Value | 10 | | --------------- | ------------------------------ | 11 | | **Module Name** | OpenHab | 12 | | **Module Type** | Energy Management System (EMS) | 13 | | **Features** | Consumption, Generation | 14 | | **Status** | Implemented, *untested* | 15 | 16 | ## Configuration 17 | 18 | The following table shows the available configuration parameters for the openHAB EMS module. 19 | 20 | | Parameter | Value | 21 | | ----------- | ------------- | 22 | | enabled | *required* Boolean value, `true` or `false`. Determines whether we will poll openHAB items. | 23 | | consumptionItem | *optional* Name of openHAB item displaying consumption. | 24 | | generationItem | *optional* Name of openHAB item displaying generation. | 25 | | serverIP | *required* The IP address of the openHAB instance. We will poll the REST HTTP API. | 26 | | serverPort | *optional* openHAB port. This is the port that we should connect to. Defaults to 8080. | 27 | 28 | ### JSON Configuration Example 29 | 30 | ``` 31 | "openHAB": { 32 | "enabled": true, 33 | "consumptionItem": "Solar_Consumption", 34 | "generationItem": "Solar_Generation", 35 | "serverIP": "192.168.1.2", 36 | "serverPort": "8080" 37 | } 38 | ``` 39 | 40 | ### Note 41 | 42 | In case that the TWC's power draw is included in the value of your **consumptionItem**, please ensure the following configuration setting is enabled in your ```config.json``` file: 43 | 44 | ``` 45 | { 46 | "config": { 47 | "subtractChargerLoad": true 48 | } 49 | } 50 | ``` 51 | 52 | ### Item Names 53 | 54 | The two settings "consumptionItem" and "generationItem" must be customized to point to the specific item names you use within openHAB. There is no default or common value for this, so it will require customization to work correctly. 55 | 56 | If you do not track one of these values (generation or consumption) via openHAB, leave the parameter blank, and it will not be retrieved. 57 | 58 | ### Item Types 59 | 60 | Make sure both items are of type *Number*. Using Number with a unit (*Number:\*) is also possible. 61 | 62 | ### openHAB .items File Example 63 | 64 | ``` 65 | // Just as a number 66 | Number Solar_Generation "Generation [%d W]" 67 | // As a number with unit 68 | Number:Power Solar_Consumption "Consumption [%d W]" 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/modules/EMS_OpenWeatherMap.md: -------------------------------------------------------------------------------- 1 | # OpenWeatherMap 2 | 3 | ## Introduction 4 | 5 | The OpenWeatherMap EMS module allows owners to track the relative expected solar generation of their location against weather data obtained from the OpenWeatherMap API. 6 | 7 | This allows for owners who don't have the ability to query their inverter generation details to estimate the generator output based on the weather data. 8 | 9 | ### API Key 10 | 11 | Use of the OpenWeatherMap API requires an API Key 12 | 13 | ### Constructing the Data Array 14 | 15 | In order to determine the output relative to the size of the system, historical data is required in order to understand the maximum rate of generation across each month of the year. 16 | 17 | The array ```PeakKW``` specified in the configuration for this module contains 12 values, one per month, which represents the peak output value for an entire day on the highest output day of that month. 18 | 19 | For example, if the 15th of March was the peak generation day for March (no clouds, peak conditions) and the highest generation value for that day was 13kW at 12PM, you would place the value 13 in the fourth element of the ```PeakKW``` array. 20 | 21 | -------------------------------------------------------------------------------- /docs/modules/EMS_P1Monitor.md: -------------------------------------------------------------------------------- 1 | # P1 Monitor EMS Module 2 | 3 | ## Introduction 4 | 5 | The P1 Monitor (https://www.ztatz.nl/) EMS module allows fetching of Consumption and Production values from the P1 Monitor Phase API (/api/v1/phase). 6 | 7 | ## How it works 8 | 9 | On each policy request this EMS module request data from the /api/v1/phase endpoint of your P1 Monitor taking a configurable number of samples. It receives both Consumption and Production data of each phase and will calculate a trimmed average (cutting of 10% of the minimum and maximum values) to get a value that is not influenced by any spikes on the net (e.g. a Quooker periodically heating up for a couple of seconds). By default the P1 Monitor API reports new values each 10 seconds, so when taking 6 samples it will give you an average Consumption/Production over 60 seconds. When having 3 phases the total Production on all phases is reported and for Consumption it will report the phase with the highest load multiplied by 3. 10 | 11 | ### Dependencies 12 | 13 | The P1Monitor module requires numpy and scipy installed locally to operate. We do not install these dependencies automatically, but you can install them with: 14 | 15 | ``` 16 | apt-get install python3-numpy python3-scipy 17 | ``` 18 | 19 | ### Status 20 | 21 | | Detail | Value | 22 | | --------------- | ------------------------------ | 23 | | **Module Name** | P1Monitor | 24 | | **Module Type** | Energy Management System (EMS) | 25 | | **Features** | Consumption, Production | 26 | | **Status** | In Development | 27 | 28 | ## Configuration 29 | 30 | The following table shows the available configuration parameters for the P1 Monitor EMS module. 31 | 32 | | Parameter | Value | 33 | | ----------- | ------------- | 34 | | serverIP | *required* The IP address of the P1 Monitor instance. | 35 | | samples | *optional* The amount of samples to calculate with (default 1, min 1, max 10). | 36 | 37 | ### JSON Configuration Example 38 | 39 | ``` 40 | { 41 | "sources":{ 42 | "P1Monitor": { 43 | "serverIP": "192.168.1.2", 44 | "samples": 1 45 | } 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/modules/EMS_Powerwall2.md: -------------------------------------------------------------------------------- 1 | # Tesla Powerwall 2 EMS Module 2 | 3 | ## Introduction 4 | 5 | The Tesla Powerwall 2 EMS module is used for fetching Solar Generation and Power Consumption values from a Tesla Powerwall 2 battery system. 6 | 7 | The Tesla Powerwall 2 sits between a Solar generation system, the home electrical system and the electricity grid. This makes it very useful for monitoring the overall energy utilization of the house. Using this EMS module, we can calcuate how much solar power is being generated at any time, how much power consumption is currently metered, and we will then consume the difference between these values for the Tesla Wall Charger. 8 | 9 | ### Note 10 | 11 | Given the location of the Powerwall in the home electrical system, it is highly likely that the TWC is drawing power from the Powerwall. As a result, the TWC's load will show as Consumption via the Powerwall's meter. If this is the case, please ensure the following configuration setting is enabled in your ```config.json``` file: 12 | 13 | ``` 14 | { 15 | "config": { 16 | "subtractChargerLoad": true 17 | } 18 | } 19 | ``` 20 | 21 | ### Status 22 | 23 | | Detail | Value | 24 | | --------------- | --------------------------------------------- | 25 | | **Module Name** | Powerwall2 | 26 | | **Module Type** | Energy Management System (EMS) | 27 | | **Features** | Consumption, Generation, Grid Status, Voltage | 28 | | **Status** | Implemented, *untested* | 29 | 30 | ## Configuration 31 | 32 | The following table shows the available configuration parameters for the Tesla Powerwall 2 EMS module. 33 | 34 | | Parameter | Value | 35 | | ----------- | ------------- | 36 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Tesla Powerwall 2. | 37 | | password | *optional* Password for the installer user, if required. If this is supplied, we will request a login token prior to API access. If not, requests will be performed without authentication. | 38 | | serverIP | *required* The IP address of the Powerwall2 device. We will poll this device's HTTPS API | 39 | | serverPort | *optional* API Server port. This is the port that we should connect to. This is almost always 443 (HTTPS) | 40 | 41 | ### JSON Configuration Example 42 | 43 | ``` 44 | "Powerwall2": { 45 | "enabled": true, 46 | "serverIP": "192.168.1.2", 47 | "serverPort": 443, 48 | "password": "test123" 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/modules/EMS_SmartMe.md: -------------------------------------------------------------------------------- 1 | # SmartMe EMS Module 2 | 3 | This module allows querying of [smart-me.com](https://smart-me.com/swagger/ui/index) power sensors. 4 | 5 | Note: Only Generation values are supported by this module. The power measured by the sensor will be evaluated as Generation if the value is Negative, or Consumption if the value is Positive. As a result, only a Generation or Consumption value should be shown at any time, but never both. 6 | 7 | ## Introduction 8 | 9 | | **Parameter** | **Value** | 10 | | ------------- | --------- | 11 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the SmartMe API | 12 | | password | *required* The password for accessing the API | 13 | | serialNumber | *required* The Serial Number of the sensor to query | 14 | | username | *required* The username for accessing the API | 15 | 16 | ## JSON Configuration Example 17 | 18 | The following configuration should be placed under the "sources" section of the config.json file in your installation, and will enable SmartMe EMS polling. 19 | 20 | ``` 21 | "SmartMe": { 22 | "enabled": true, 23 | "username": "username", 24 | "password": "password", 25 | "serialNumber": "ABC1234" 26 | } 27 | 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/modules/EMS_SmartPi.md: -------------------------------------------------------------------------------- 1 | # SmartPi EMS Module 2 | 3 | This module allows querying of [SmartPi](https://www.enerserve.eu/en/smartpi.html) power sensors. 4 | 5 | Note: Only Generation values are supported by this module. The power measured by the sensor will be evaluated as Generation if the value is Negative, or Consumption if the value is Positive and ```showConsumption``` is enabled, otherwise zero.. As a result, only a Generation or Consumption value should be shown at any time, but never both. 6 | 7 | ## Introduction 8 | 9 | | **Parameter** | **Value** | 10 | | ------------- | --------- | 11 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the SmartPi Device | 12 | | serverIP | *required* The IP Address of the SmartPi Device | 13 | | serverPort | *required* The Port on which the SmartPi API is reachable | 14 | | showConsumption | *optional* Determines whether we should show a negative Generation figure as zero (False, default), or as a positive Consumption figure (True). Note that this should not be enabled if you're using ```subtractChargerLoad``` in your ```config.json```. | 15 | 16 | ## JSON Configuration Example 17 | 18 | The following configuration should be placed under the "sources" section of the config.json file in your installation, and will enable SmartPi EMS polling. 19 | 20 | ``` 21 | "SmartPi": { 22 | "enabled": true, 23 | "serverIP": "192.168.1.2", 24 | "serverPort": "1080" 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/modules/EMS_SolarEdge.md: -------------------------------------------------------------------------------- 1 | # SolarEdge EMS Module 2 | 3 | ## Introduction 4 | 5 | The SolarEdge EMS Module allows energy generation to be fetched from the SolarEdge API (```monitoringapi.solaredge.com```), which is a hosted management portal for SolarEdge inverters. 6 | 7 | The module supports fetching generation values via the ```summary``` API, with efforts to fetch consumption via the ```siteCurrentPowerFlow``` API underway. 8 | 9 | ### A note about API Limits 10 | 11 | Currently, SolarEdge's API interface limits users to 300 API requests per day. At a rate of one query per 90 seconds (which is what we limit the request rate to in this module), you are able to query for 7.5 hours per day. 12 | 13 | Within the module itself, when you first start TWCManager, you might find the resolution of your generation rate is lower than after the first 5 polls, if you do not have a consumption meter. This is because: 14 | 15 | * The SolarEdge API publishes two endpoints, one with a high resolution generation value and no consumption, and one with a low-resolution generation and consumption value. 16 | * For the first 3 polls, we always use the low resolution endpoint while we measure if your installation provides a consumption value or not. 17 | * At this point, the module will either remain with the lower resolution endpoint with consumption support, or switch back to the higher resolution endpoint if no consumption value exists. 18 | 19 | Perhaps in future if the API query limits are more generous, we could both drop the cache time and query both endpoints to enhance the experience, but this is about the best mid-point of functionality vs usability we can offer. 20 | 21 | ## Configuration 22 | 23 | The following table shows the available configuration parameters for the SolarEdge EMS Module: 24 | 25 | | **Parameter** | **Value** | 26 | | ------------- | --------- | 27 | | debugFile | *optional* When used with the ```debugMode``` parameter below, specifies the location of the debug log file that SolarEdge generates. Default is ```/tmp/twcmanager_solaredge_debug.txt```. Make sure twcmanager has permissions to write to the directory/file. | 28 | | debugMode | *optional* If set to 1, enables Debug Logging which logs every request and reply to/from the SolarEdge API for analysis by developers. Set to 0 by default. | 29 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the SolarEdge API | 30 | | pollMode | *optional* Allows a static definition of which poll mode to use. The default is 0 (auto-detect) however this can be set to 1 (non-consumption) or 2 (consumption) | 31 | | apiKey | *required* The API Key assigned when creating a new SolarEdge account via the portal. | 32 | | siteID | *required* The site ID assigned when creating a new SolarEdge account via the portal. | 33 | 34 | Please note, if any of the required parameters for the SolarEdge EMS module are not specified in the module configuration, the module will unload at start time. 35 | 36 | ## JSON Configuration Example 37 | 38 | ``` 39 | "SolarEdge": { 40 | "enabled": true, 41 | "apiKey": "abcdef", 42 | "siteID": "ghijec", 43 | "debugMode": false 44 | } 45 | ``` 46 | 47 | ## Debugging 48 | 49 | To debug the SolarEdge EMS module, you are recommended to run the module at debugLevel 4. This will print an error message if: 50 | 51 | * Any exception is triggered when trying to update the SolarEdge consumption value (however the actual exception will be printed at debugLevel 10). 52 | * If the current generation value (under ) is not found in the XML returned by the SolarEdge API. 53 | 54 | If these messages are not sufficient to find an issue with the module, please use the ```debugMode``` parameter. 55 | -------------------------------------------------------------------------------- /docs/modules/EMS_SolarLog.md: -------------------------------------------------------------------------------- 1 | # SolarLog EMS Module 2 | 3 | ## Introduction 4 | 5 | The SolarLog EMS Module allows energy generation and production to be fetched from the Solar-Log Base API (```https://www.solar-log.com/de/produkte-komponenten/solar-logTM-hardware/solar-log-base/```) that generates a webservice directly on the device 6 | 7 | ## Configuration 8 | 9 | The following table shows the available configuration parameters for the SolarLog EMS Module: 10 | 11 | | **Parameter** | **Value** | 12 | | ------------- | --------- | 13 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the SolarLog API | 14 | | serverIP | *required* The IP Address of the Solar-Log Base device. | 15 | | excludeConsumptionInverters | *optional and experimental* Indices of reading devices - to exclude consumption from. Needed if e.g. a boiler only heats with solar overhead - and you want to override this one. (it works for me - but there are perhaps some other use cases) | 16 | 17 | Please note, if any of the required parameters for the SolarLog EMS module are not specified in the module configuration, the module will unload at start time. 18 | 19 | ## JSON Configuration Example 20 | 21 | ``` 22 | "SolarLog": { 23 | "enabled": false, 24 | "serverIP": "192.168.1.2", 25 | "excludeConsumptionInverters": [2, 3] 26 | }, 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /docs/modules/EMS_TED.md: -------------------------------------------------------------------------------- 1 | # The Energy Detective EMS Module 2 | 3 | ## Introduction 4 | 5 | ### Status 6 | 7 | | Detail | Value | 8 | | --------------- | ------------------------------ | 9 | | **Module Name** | TED | 10 | | **Module Type** | Energy Management System (EMS) | 11 | | **Features** | Generation | 12 | | **Status** | Implemented, *untested* | 13 | 14 | ## Configuration 15 | 16 | The following table shows the available configuration parameters for the TED EMS module. 17 | 18 | | Parameter | Value | 19 | | ----------- | ------------- | 20 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll TED. | 21 | | serverIP | *required* The IP address of the TED device. We will poll this device's API. | 22 | | serverPort | *optional* TED Web Server port. This is the port that we should connect to. | 23 | 24 | ### JSON Configuration Example 25 | 26 | ``` 27 | "TED": { 28 | "enabled": true, 29 | "serverIP": "192.168.1.1", 30 | "serverPort": 80 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/modules/EMS_Volkszahler.md: -------------------------------------------------------------------------------- 1 | # Volkszahler 2 | 3 | The Volkszahler EMS module allows reading of a meter value from the Volkszahler platform which represents the Solar Generation value for a meter managed by Volkszahler. 4 | 5 | ## Configuration 6 | 7 | The following table shows the available configuration parameters for the Volkszahler EMS Module: 8 | 9 | | **Parameter** | **Value** | 10 | | ------------- | --------- | 11 | | enabled | *required* Boolean value, ```true``` or ```false```. Determines whether we will poll the Volkszahler Server | 12 | | serverIP | *required* The IP address of the Volkszahler server that we will query for the generation value | 13 | | serverPort | *required* The Server Port that we will query. This is the port that the front-end UI uses, not the VZLOGGER port. | 14 | | uuid | *required* The UUID of the channel "Total Consumption" of your houshold grid meter. 15 | 16 | ## UUID Creation 17 | 18 | The following steps explain how to create the Channel in Volkszähler 19 | 20 | * Step 1: open Frontend and click "Kanal hinzufügen"(= add channel) 21 | * Step 2: in next window select tab "Kanal erstellen" (= create new channel) and fill any parameter. 22 | Then the channel got a new random UUID (is like GUID) 23 | * Step 3: after the dialog was closed is the channel was added below and an the mot richt column is a blue "i" : There you can read the new UUID. 24 | * Step 4: Now edit the VZLOGGER config file. Here you fill in the UUID and define where you get data and the format. 25 | 26 | Then the VZLOGGER service will do the rest. VZLOGGER is best for reading electricity meters (there exist also plugin for OCR reading from Video). Since data is pushed by a simple URL to database, you can also use any own active device, which knows the syntax. 27 | 28 | My electricity meter has 4 readings: 3 counters kWh and a Watt display. You have to try baud, parity, protocol and other stuff to find correct format. 29 | My electricity meter Watt display show + and - , some have two separate values both +, this depend on the meter manufacturer. 30 | 31 | Url to try out reading: http:///api/data.txt?from=now&uuid=00000000-1111-1670-ffff-0123456789ab 32 | Use your own UUID of the channel "Total Consumption" of your houshold grid meter. (see at blue "i") 33 | Reading must be negative on "Consumption < solar production". TWCManager uses negative value as "green energy". 34 | If you have only positive value than just create a new virtual channel fill in 'Regel'(=calulation formula): "-val(in1)" 35 | Example reading: "-5520.8 W" (=Watt). 36 | -------------------------------------------------------------------------------- /docs/modules/Interface_Dummy.md: -------------------------------------------------------------------------------- 1 | # Dummy Interface 2 | 3 | ## Introduction 4 | 5 | The Dummy Interface enables testing of the TWCManager engine by simulating a TWC device 6 | 7 | It is in active development, and further documentation will follow. 8 | -------------------------------------------------------------------------------- /docs/modules/Interface_RS485.md: -------------------------------------------------------------------------------- 1 | # RS485 Communication Interface 2 | 3 | ## Introduction 4 | 5 | The RS485 Interface Module provides Serial and (limited) Network connectivity between TWCManager and Slave TWCs. 6 | 7 | You should use this module if: 8 | 9 | * Your device is directly connected to the TWC devices via a physical RS485 (2-wire) connection, either via USB device, shield, etc. 10 | * Your device connects over the network to an RS485 to TCP converter which is supported via the Raw or RFC2217 (Telnet) protocols without encryption. 11 | 12 | ## Serial 13 | 14 | Serial communications with TWC is established through the use of a Serial Device, such as the default setting of ```/dev/ttyUSB0``` which represents the first USB to Serial device connected to a machine. 15 | 16 | Some examples of Serial configuration include: 17 | 18 | ```"device": "/dev/ttyUSB0"``` - Connection to a USB to RS485 adaptor 19 | ```"device": "/dev/ttyS0"``` - Connection to an onboard Serial interface 20 | 21 | ## Network Communications 22 | 23 | There are two different network protocols which are supported by the RS485 module. These are less configurable than the alternate planned TCP module, however the TCP module is not yet available for use. 24 | 25 | * rfc2217 - Telnet to Serial 26 | * socket - Raw network to Serial 27 | 28 | Generally, a device will provide documentation which describes the network encoding used. 29 | 30 | Examples of the configuration of these protocols: 31 | 32 | ```"device": "rfc2217://192.168.1.2:4000/"``` 33 | 34 | ```"device": "socket://192.168.1.2:4000/"``` 35 | -------------------------------------------------------------------------------- /docs/modules/Interface_TCP.md: -------------------------------------------------------------------------------- 1 | # TCP Interface Module 2 | 3 | ## Introduction 4 | 5 | This interface is under development. It is not yet ready for use. 6 | 7 | Please review the [RS485](Interface_RS485.md) module documentation to see if this the appropriate module to use 8 | -------------------------------------------------------------------------------- /docs/modules/Logging_CSV.md: -------------------------------------------------------------------------------- 1 | # CSV Logging 2 | 3 | ## Introduction 4 | 5 | The CSV logging module outputs statistics which are printed to the console on a regular basis to a set of CSV files in a directory specified in the configuration file. 6 | 7 | This module is disabled by default. You might want this module enabled in your environment so you can log real-time status updates as vehicles charge, retaining that information locally. 8 | 9 | ## CSV Files 10 | 11 | The following CSV files will be created in the destination directory. Note that this is assuming none of the logging categories are muted. Muted categories will not be written out to file. 12 | 13 | * chargesessions.csv 14 | * Stores charge session information 15 | * greenenergy.csv 16 | * When green energy policy is in effect, logs the green energy generation and consumption data 17 | * slavestatus.csv 18 | * Logs Slave TWC status data - lifetime kWh and voltage per phase across al 19 | 20 | ## Configuration Options 21 | 22 | The following options exist for this Logging module: 23 | 24 | | Option | Example | Description | 25 | | ------- | -------- | ----------- | 26 | | enabled | *false* | Boolean value determining if the CSV logging module should be activated. The default is *false*. | 27 | | path | */etc/twcmanager/csv* | *required* A path to create the CSV files under. Make sure you make this path writable to the user that TWCManager runs as. | 28 | 29 | ### Muting Logging Topics 30 | 31 | Logging modules allow for the individual toggling of certain topics to filter output. This is entirely optional and will default to output of all topics if it does not exist under the module's configuration. Below are the topics that may be toggled: 32 | 33 | ``` 34 | "mute":{ 35 | "ChargeSessions": false, 36 | "GreenEnergy": false, 37 | "SlavePower": false, 38 | "SlaveStatus": false 39 | } 40 | ``` 41 | 42 | Setting a topic to true will cause that topic's output to be muted. 43 | 44 | ### Example Configuration 45 | 46 | Below is an example configuration for this module. 47 | 48 | ``` 49 | "logging":{ 50 | "CSV": { 51 | "enabled": true, 52 | "path": "/etc/twcmanager/csv" 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/modules/Logging_Console.md: -------------------------------------------------------------------------------- 1 | # Console Logging 2 | 3 | ## Introduction 4 | 5 | The console logging module replaces existing functionality which was included in the TWCManager daemon for printing status information to the console. 6 | 7 | This module is enabled by default. You might want this module enabled in your environment so you can monitor real-time status updates as vehicles charge. 8 | 9 | ## Configuration Options 10 | 11 | There are currently no specific configuration options for this module, other than enabling or disabling the module, and muting certain messages, both of which are universal options for logging modules 12 | 13 | | Option | Example | Description | 14 | | ------- | ------- | ----------- | 15 | | enabled | *true* | Boolean value determining if the console logging module should be activated. The default is *true*. | 16 | 17 | ### Muting Logging Topics 18 | 19 | Logging modules allow for the individual toggling of certain topics to filter output. This is entirely optional and will default to output of all topics if it does not exist under the module's configuration. Below are the topics that may be toggled: 20 | 21 | ``` 22 | "mute":{ 23 | "ChargeSessions": false, 24 | "GreenEnergy": false, 25 | "SlavePower": false, 26 | "SlaveStatus": false 27 | } 28 | ``` 29 | 30 | Setting a topic to true will cause that topic's output to be muted. 31 | 32 | ### Example Configuration 33 | 34 | Below is an example configuration for this module. 35 | 36 | ``` 37 | "logging":{ 38 | "Console": { 39 | "enabled": true 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/modules/Logging_Files.md: -------------------------------------------------------------------------------- 1 | # File Logging 2 | 3 | ## Introduction 4 | 5 | The File logging module the debug information and statistical information which are printed to the console on a regular basis to a set of Log files in a directory specified in the configuration file. 6 | 7 | This module is disabled by default. You might want this module enabled in your environment so you can log real-time status updates as vehicles charge, retaining that information locally. 8 | 9 | A new file will be created every hour. The files are deleted after 24 hours. 10 | 11 | Use this logging with care, it can break you SD card. 12 | 13 | ## Configuration Options 14 | 15 | The following options exist for this Logging module: 16 | 17 | | Option | Example | Description | 18 | | ------- | -------- | ----------- | 19 | | enabled | *false* | Boolean value determining if the logging module should be activated. The default is *false*. | 20 | | path | */etc/twcmanager/log* | *required* A path to create the log files under. Make sure you make this path writable to the user that TWCManager runs as. | 21 | 22 | ### Muting Logging Topics 23 | 24 | Logging modules allow for the individual toggling of certain topics to filter output. This is entirely optional and will default to output of all topics if it does not exist under the module's configuration. Below are the topics that may be toggled: 25 | 26 | ``` 27 | "mute":{ 28 | "ChargeSessions": false, 29 | "GreenEnergy": false, 30 | "SlavePower": false, 31 | "SlaveStatus": false, 32 | "DebugLogLevelGreaterThan": 1 33 | } 34 | ``` 35 | 36 | Setting a topic to true will cause that topic's output to be muted. With the DebugLogLevelGreaterThan parameter you can define which levels of debug information you want to write into the file. As higher the value, the more info will be written to the file (values between 0 and 12). 0 will mute the debug information completely. 37 | 38 | ### Example Configuration 39 | 40 | Below is an example configuration for this module. 41 | 42 | ``` 43 | "logging":{ 44 | "FileLogger": { 45 | "enabled": true, 46 | "path": "/etc/twcmanager/log", 47 | "mute":{ 48 | "ChargeSessions": false, 49 | "GreenEnergy": false, 50 | "SlavePower": false, 51 | "SlaveStatus": false, 52 | "DebugLogLevelGreaterThan": 8 53 | } 54 | }, 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/modules/Logging_MySQL.md: -------------------------------------------------------------------------------- 1 | # MySQL Logging 2 | 3 | ## Introduction 4 | 5 | The MySQL logging module allows the storage of statistics in a MySQL database either locally or on a remote machine. 6 | 7 | This module is disabled by default. You might want this module enabled in your environment if you would like to log statistics from your TWCManager installation. In particular, you may find this module useful if you would like to log externally to the device you are running TWCManager on. 8 | 9 | ## Dependency Warning 10 | 11 | There is an extra python3 dependency required if you would like to use MySQL as a Logging module, this is not installed automatically using setup.py: 12 | 13 | ```pip3 install pymysql``` 14 | 15 | ## Configuration Options 16 | 17 | The following configuration parameters exist for this logging module: 18 | 19 | | Option | Example | Description | 20 | | -------- | ------- | ----------- | 21 | | database | *twcmanager* | *required* The name of the database that you would like to log to on the MySQL host. | 22 | | enabled | *false* | *required* Boolean value determining if the console logging module should be activated. The default is *false*. | 23 | | host | *10.10.10.5* | *required* The hostname or IP address of the MySQL server that you would like to log to. | 24 | | port | 3306 | *optional* The port of the MySQL server that you would like to log to. | 25 | | password | *abc123* | *required* The password to use. | 26 | | username | *twcmanager* | *required* The username to use. | 27 | 28 | ### Muting Logging Topics 29 | 30 | Logging modules allow for the individual toggling of certain topics to filter output. This is entirely optional and will default to output of all topics if it does not exist under the module's configuration. Below are the topics that may be toggled: 31 | 32 | ``` 33 | "mute":{ 34 | "ChargeSessions": false, 35 | "GreenEnergy": false, 36 | "SlavePower": false, 37 | "SlaveStatus": false 38 | } 39 | ``` 40 | 41 | Setting a topic to true will cause that topic's output to be muted. 42 | 43 | ### Example Configuration 44 | 45 | ``` 46 | "logging":{ 47 | "MySQL": { 48 | "enabled": false, 49 | "host": "1.2.3.4", 50 | "port": 3306, 51 | "database": "twcmanager", 52 | "username": "twcmanager", 53 | "password": "twcmanager" 54 | } 55 | ``` 56 | 57 | ## Database Schema 58 | 59 | Unlike the SQLite database, the MySQL database logging module currently requires that you create the database schema manually from the SQL below on the target database server. 60 | 61 | The following is the database schema for **v1.2.0** of TWCManager 62 | 63 | ``` 64 | CREATE TABLE charge_sessions ( 65 | chargeid int, 66 | startTime datetime, 67 | startkWh int, 68 | slaveTWC varchar(4), 69 | endTime datetime, 70 | endkWh int, 71 | vehicleVIN varchar(17), 72 | primary key(startTime, slaveTWC) 73 | ); 74 | 75 | CREATE TABLE green_energy ( 76 | time datetime, 77 | genW DECIMAL(9,3), 78 | conW DECIMAL(9,3), 79 | chgW DECIMAL(9,3), 80 | primary key(time) 81 | ); 82 | 83 | CREATE TABLE slave_status ( 84 | slaveTWC varchar(4), 85 | time datetime, 86 | kWh int, 87 | voltsPhaseA int, 88 | voltsPhaseB int, 89 | voltsPhaseC int, 90 | primary key (slaveTWC, time)); 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/modules/Logging_SQLite.md: -------------------------------------------------------------------------------- 1 | # SQLite Logging Module 2 | 3 | Development on this module is on pause. 4 | 5 | The reason: It has added complexity for implementation above and beyond that which exists for CSV, yet it can only be used locally unlike MySQL. 6 | 7 | It is recommended to use CSV for local or MySQL for remote database in the meantime, until there's enough momentum for this module to be completed 8 | -------------------------------------------------------------------------------- /docs/modules/Logging_Sentry.md: -------------------------------------------------------------------------------- 1 | # Sentry 2 | 3 | ## Introduction 4 | 5 | Sentry is an open-source SaaS full-stack error tracking system. 6 | 7 | Using sentry, applications can be profiled and monitored for errors across releases, with a user-friendly web based interface to track and manage errors. 8 | 9 | ## Installation 10 | 11 | Note: Using Sentry requires a minimum Python version of 3.4. 12 | 13 | All dependencies and prerequisites for using Sentry are installed automatically as part of the ```setup.py``` script. 14 | 15 | ## Step by Step Setup 16 | 17 | 1 Sign up to https://sentry.io 18 | 19 | 2 Create a project for HomeAssistant 20 | 21 | 3 Select Python as the platform 22 | 23 | 4 Give the project a name 24 | 25 | 5 From the Configure Python screen, copy the URL specified in the sentry_sdk.init funciton, and paste it into the DSN configuration field in ```config.yaml``` 26 | -------------------------------------------------------------------------------- /docs/modules/Status_HASS.md: -------------------------------------------------------------------------------- 1 | # HomeAssistant Status Module 2 | 3 | ## Introduction 4 | 5 | The HomeAssistant Status Module provides a mechanism to publish sensor states to HomeAssistant via the HomeAssistant API. 6 | 7 | The module uses a long-term access key that is created through the HASS web interface, which allows the module to send updates to the sensors listed below without needing hard-coded credentials. 8 | 9 | ## HomeAssistant Sensors 10 | 11 | The following sensors and their values are published to HomeAsssitant via the HomeAssistant HTTP API. 12 | 13 | | HomeAssistant Sensor | Value | Example | 14 | | ---------------------------------------- | ------------------------------------ | ----- | 15 | | sensor.twcmanager_all_total_amps_in_use | Float: Amps in use across all slaves | 16.24 | 16 | | sensor.twcmanager_*charger*_amps_in_use | Float: Amps in use for given Slave TWC | 8.52 | 17 | | sensor.twcmanager_*charger*_amps_max | Integer: Reported maximum amperage per Slave TWC | 32 | 18 | | sensor.twcmanager_*charger*_cars_charging | Boolean: Will be 0 if Slave TWC does not have a connected charging vehicle, or 1 if it does. | 19 | | sensor.twcmanager_*charger*_charger_load_w | Integer: Actual power being consumed as reported by the Slave TWC. | 2977 | 20 | | sensor.twcmanager_*charger*_current_vehicle_vin | String: The VIN of a vehicle currently charging from this Slave TWC. | 21 | | sensor.twcmanager_*charger*_last_vehicle_vin | String: The VIN of the vehicle previously charging from this Slave TWC. | 22 | | sensor.twcmanager_*charger*_lifetime_kwh | Integer: Lifetime kWh output by specified charger. | 159 | 23 | | sensor.twcmanager_*charger*_power | Float: Actual amps being consumed as reported by the Slave TWC. | 14.51 | 24 | | sensor.twcmanager_*charger*_state | Integer: State code reported by TWC. Please see table below for state codes. | 25 | | sensor.twcmanager_*charger*_voltage_phase_*phase* | Integer: Volts per phase (a/b/c) per Slave TWC | 243 | 26 | | sensor.twcmanager_config_min_amps_per_twc | Integer: Minimum amps to charge per TWC (from config) | 6 | 27 | | sensor.twcmanager_config_max_amps_for_slaves | Integer: Total number of amps on power circuit to divide amongst Slave TWCs | 32 | 28 | 29 | ### State Codes 30 | 31 | The following state codes are reported by Slave TWCs: 32 | 33 | | State Code | Description | 34 | | ---------- | ----------- | 35 | | 0 | Ready. Car may or may not be plugged in to TWC | 36 | | 1 | Plugged in and charging | 37 | | 2 | Error | 38 | | 3 | Plugged in, do not charge (vehicle finished charging or error) | 39 | | 4 | Plugged in, ready to charge or charge is scheduled | 40 | | 5 | Busy | 41 | | 6 | TBA. Version 2 only. | 42 | | 7 | TBA. Version 2 only. | 43 | | 8 | Starting to charge. | 44 | | 9 | TBA. Version 2 only. | 45 | | 10 | Amp adjustment period complete. | 46 | | 15 | Unknown. | 47 | -------------------------------------------------------------------------------- /docs/modules/Status_MQTT.md: -------------------------------------------------------------------------------- 1 | # MQTT Status Module 2 | 3 | ## Introduction 4 | 5 | The MQTT Status Module provides a mechanism to publish MQTT topic values to an MQTT broker. 6 | 7 | The module uses supplied credentials to connect to the MQTT server, and publishes status updates. The name of the MQTT topics used are prefixed with a specified prefix, which is configured as TWC by default in the configuration file. 8 | 9 | ## MQTT Topics 10 | 11 | | MQTT Topic | Value | Example | 12 | | -------------------------------- | -------------------------------------- | ------- | 13 | | *prefix*/all/totalAmpsInUse | Float: Amps in use across all slaves | 16.24 | 14 | | *prefix*/*charger*/ampsInUse | Float: Amps in use for given Slave TWC | 8.52 | 15 | | *prefix*/*charger*/ampsMax | Integer: Reported maximum amperage per Slave TWC | 32 | 16 | | *prefix*/*charger*/carsCharging | Boolean: Will be 0 if Slave TWC does not have a connected charging vehicle, or 1 if it does. | 17 | | *prefix*/*charger*/currentVehicleVIN | String: The VIN of a vehicle currently charging from this Slave TWC. | 18 | | *prefix*/*charger*/lastVehicleVIN | String: The VIN of the vehicle previously charging from this Slave TWC. | 19 | | *prefix*/*charger*/lifetimekWh | Integer: Lifetime kWh output by specified charger. | 159 | 20 | | *prefix*/*charger*/power | Float: Actual amps being consumed as reported by the Slave TWC. | 14.51 | 21 | | *prefix*/*charger*/state | Integer: State code reported by TWC. Please see table below for state codes. | 22 | | *prefix*/*charger*/voltagePhase*X* | Integer: Volts per phase (a/b/c) per Slave TWC | 243 | 23 | | *prefix*/config/maxAmpsForSlaves | Integer: Total number of amps on power circuit to divide amongst Slave TWCs | 32 | 24 | | *prefix*/config/minAmpsPerTWC | Integer: Minimum amps to charge per TWC (from config) | 6 | 25 | 26 | ### Rate limiting 27 | 28 | By default, the MQTT Status Module will limit one update per topic per 60 seconds. The reason for this is that MQTT publishing is an asynchronous process. Updates are queued and sent in order to the MQTT broker. If there was a delay in publishing and acknowledging the MQTT messages on the broker side, the queue would continue to grow and the message buffer would consume more memory until it eventually ran out of available memory. 29 | 30 | This isn't going to be an issue in all environments, but defaulting to a configuration that cannot effectively handle latency would be a worse situation. If you'd like to reduce or even disable the rate limiting, you can adjust it in the configuration file: 31 | 32 | ``` 33 | "status": { 34 | "MQTT": { 35 | "ratelimit": {number in seconds for how many seconds per update per topic, or 0 to disable) 36 | } 37 | } 38 | ``` 39 | 40 | ### State Codes 41 | 42 | The following state codes are reported by Slave TWCs: 43 | 44 | | State Code | Description | 45 | | ---------- | ----------- | 46 | | 0 | Ready. Car may or may not be plugged in to TWC | 47 | | 1 | Plugged in and charging | 48 | | 2 | Error | 49 | | 3 | Plugged in, do not charge (vehicle finished charging or error) | 50 | | 4 | Plugged in, ready to charge or charge is scheduled | 51 | | 5 | Busy | 52 | | 6 | TBA. Version 2 only. | 53 | | 7 | TBA. Version 2 only. | 54 | | 8 | Starting to charge. | 55 | | 9 | TBA. Version 2 only. | 56 | | 10 | Amp adjustment period complete. | 57 | | 15 | Unknown. | 58 | 59 | -------------------------------------------------------------------------------- /docs/modules/Vehicle_TeslaBLE.md: -------------------------------------------------------------------------------- 1 | Tesla Bluetooth Low Energy (BLE) 2 | ================================ 3 | 4 | ## Introduction 5 | 6 | This module provides experimental support for the Tesla BLE (Bluetooth Low Energy) interface to Tesla vehicles. 7 | 8 | It is currently capable of starting and stopping charging, and controlling the charge rate in line with the configurable charge rate options (Settings > Charge Rate Control). 9 | 10 | ## Installation of tesla-control binary 11 | 12 | The tesla-control binary can be built for you via the TWCManager Makefile. 13 | 14 | ### Installation Steps 15 | 16 | * Update twcmanager to at least vx.xx to introduce Tesla BLE support (you could alternatively do this via the web-based update option) 17 | 18 | ```make install``` 19 | 20 | * Install golang and the tesla-command BLE proxy 21 | 22 | ```make tesla-control``` 23 | 24 | This will download the required golang distribution, and build the tesla-control binary in the default HOME directory, which is ```/home/twcmanager```. You may have an alternate home location that you would like to use, in which case you would specify make HOME=x tesla-control. 25 | 26 | If you would prefer to build tesla-control yourself, or you are able to install it as part of a binary package distribution, you can skip this step. 27 | 28 | * Restart twcmanager 29 | 30 | ```systemctl restart twcmanager``` 31 | 32 | ### Configuring binary path 33 | 34 | If you have installed tesla-control into a path which is outside of your system $PATH, or if TWCManager is unable to detect the location of tesla-control, you can configure the 35 | 36 | ``` 37 | "config": { 38 | "teslaControlPath": "/usr/local/bin/tesla-control-local", 39 | ``` 40 | 41 | ### Peering with your vehicle 42 | 43 | Once TWCManager restarts, it will check for the tesla-control command. If this exists, you will see a new option under the Vehicles > [Vehicle VIN] menu, under the heading BLE Control. There will be an option to pair with the vehicle. 44 | 45 | * Ensure the vehicle is near to your TWCManager device, and that you have your key card available. 46 | * Click the Pair with Vehicle button, which will initiate Bluetooth Pairing 47 | * Enter the vehicle, tap your card on the dash and accept the peering 48 | 49 | ### Controlling charge rate using tesla-control 50 | 51 | By default, TWCManager uses the TWC to control charge rate. This behaviour is configurable through the Web UI. To change this behaviour to leverage the BLE interface for controlling charge rate, perform the following steps: 52 | 53 | * Click on the Settings tab 54 | * Find the option "Charge Rate Control" 55 | * Select either of the following options: 56 | * Use Tesla API Exclusively to control Charge Rate 57 | * Use TWC >= 6A + Tesla API <6A to control Charge Rate 58 | 59 | ## FAQs 60 | 61 | * *Why does BLE currently require API as a fallback?* 62 | 63 | TWCManager was written with an explicit dependency on the API. BLE replaces some but not all of the API's functionality. For example, we cannot discover all vehicles on an account with BLE. 64 | 65 | The approach taken is to replace the API calls where an appropriate BLE replacement exists. This has the benefit of using API only as a fallback when BLE fails, which improves reliability. 66 | 67 | * *What are the BLE-specific considerations that need to be taken into account?* 68 | 69 | BLE being a local wireless technology introduces a number of considerations. The distance between the TWC and the vehicle is important to consider, as weak signals can contribute to instability. If distance is a challenge, there are potential solutions such as an ESP-based tesla-control interface, however support for this does not exist yet. 70 | 71 | * *I tried to use the ```teslaControlPath``` setting to call a script which then goes on to call tesla-control. After a period of time, TWCManager background tasks seem to get stuck. Why? 72 | 73 | The tesla-control CLI interface has a number of failure cases in which it will hang indefinitely. We work around this by terminating the process after 10 seconds of inactivity, however this does not kill the entire process tree. If you are running the command through some sort of wrapper, it will become zombified whilst the tesla-control command will remain. 74 | 75 | If you are attempting to wrap the tesla-control command and want to confirm that this is the issue, perform a ``ps aux | grep tesla-control``` and look for a process which was spawned more than 10 seconds ago. 76 | 77 | You can work around this with a similar timeout in the wrapper script, eg ```timeout 9 tesla-control $@``` 78 | -------------------------------------------------------------------------------- /docs/modules/Vehicle_TeslaMate.md: -------------------------------------------------------------------------------- 1 | # TeslaMate Vehicle Integration 2 | 3 | ## Introduction 4 | 5 | TeslaMate is a self-hosted Tesla datalogger. 6 | 7 | The purpose of this integration is to allow users of TWCManager who also use TeslaMate to leverage on the work already performed by TeslaMate to fetch vehicle status, significantly reducing the number of Tesla API calls that TWCManager needs to make. 8 | 9 | With both the API Token and Telemetry options enabled, we're effectively using TeslaMate to manage our API login credentials and fetch all of our vehicle status updates. The only requests that TWCManager would need to send are the initial API call to list all vehicles in your account, and commands (such as wake_up, charge, etc). 10 | 11 | There is no requirement to use both sync functions when integrating with TeslaMate. You can sync only tokens, or only telemetry. 12 | 13 | ## Integrations 14 | 15 | ### Tesla API Tokens 16 | 17 | TWCManager can synchronize the TeslaMate API tokens from the TeslaMate database. This allows token management to be performed on our behalf by TeslaMate rather than within TWCManager. 18 | 19 | This is performed through direct queries of the TeslaMate database, and is not used by the telemetry feature of the TeslaMate module, which is separate. You do not need to use the API Token sync function even if you do use the telemtry sync function. 20 | 21 | Please note that turning on the Token Sync functionality of this module will have the following affect on TWCManager's Tesla API token handling functionality: 22 | 23 | * You will no longer be prompted for your Tesla API login. Even if the TeslaMate tokens are invalid, you will not be prompted. 24 | * TWCManager will not refresh API tokens, however on refresh of the TeslaMate token, the token will be used by TWCManager. 25 | 26 | #### Configuration 27 | 28 | ``` 29 | "vehicle": { 30 | "TeslaMate": { 31 | "enabled": true, 32 | "syncTokens": true, 33 | "db_host": "192.168.1.1", 34 | "db_name": "teslamate", 35 | "db_pass": "teslamate", 36 | "db_user": "teslamate" 37 | } 38 | } 39 | ``` 40 | 41 | #### Access to Database Instance 42 | 43 | By default, TWCManager may not be able to access the database for TeslaMate. 44 | 45 | Depending on your TeslaMate installation type, you may need to take different steps to make the PostgreSQL database available to TWCManager in order to sync Tesla auth tokens. Follow the appropriate section below for your installation type. 46 | 47 | ##### Manual Installation 48 | 49 | For a manual installation, the ```/etc/postgresql/pg_hba.conf``` file will need to be updated to allow the TWCManager host to connect remotely to the database. 50 | 51 | Add the following line below to pg_hba.conf, substituting the IP address of your TWCManager machine: 52 | 53 | ``` 54 | host teslamate teslamate 192.168.1.1/32 md5 55 | ``` 56 | 57 | After adding this, reload the PostgreSQL configuration: 58 | 59 | ``` 60 | service postgresql reload 61 | ``` 62 | 63 | ##### Docker Installation 64 | 65 | To be updated - I am not using this configuration, I welcome feedback if you do. 66 | 67 | ### Telemetry Information 68 | 69 | TWCManager can use the MQTT topics advertised by TeslaMate to recieve charge state and SoC information from your vehicle 70 | 71 | #### Confirming Telemetry Flow 72 | 73 | Once you have MQTT connectivity set up for TeslaMate telemetry, you should see the following log entry appear for each of the vehicles tracked by TeslaMate, as long as they are within your Tesla API credential account, which indicates that we have detected Telemetry information for this vehicle: 74 | 75 | 00:06:58 🚗 TeslaMat 20 Vehicle R*display_name* telemetry being provided by TeslaMate 76 | 77 | This indicates that we have internally switched off telemetry polling to the Tesla API for this vehicle, to instead obtain the information from TeslaMate. 78 | 79 | Note however that TeslaMate has a health indicator which indicates whether there is an issue with a TeslaMate vehicle's tracking or connectivity. If we see that the health indicator indicates an issue, we will stop tracking the telemetry via TeslaMate until it shows healthy again, and the following message will appear: 80 | 81 | IN PROGRESS 82 | 83 | It is possible for TeslaMate to lose connection to the MQTT server and for the data served to become stale. If this occurs, TWCManager will detect it within 1 hour of the data going stale if the vehicle was online at the time. Currently, if the vehicle was offline at the time it will not be detected. 84 | 85 | Once this is detected, TWCManager will print an error message similar to the below, and revert to API polling again: 86 | 87 | ``` 88 | 18:03:25 🚗 TeslaAPI 40 Data from TeslaMateVehicle for *display_name* is stale. Switching back to TeslaAPI 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/addConsumptionOffset.md: -------------------------------------------------------------------------------- 1 | # addConsumptionOffset API Command 2 | 3 | ## Introduction 4 | 5 | The addConsumptionOffset API command requests TWCManager to add a new consumption offset, or edit an existing consumption offset. The Primary Key is the name of the offset. 6 | 7 | ## Format of request 8 | 9 | The addConsumptionOffset API command is accompanied by a payload which describes the offset that you are configuring or editing. The following example payload shows a request to add or edit an offset called WattsOffset with a positive offset expressed in watts: 10 | 11 | ``` 12 | { 13 | "offsetName": "WattsOffset", 14 | "offsetValue": 500.5, 15 | "offsetUnit": "W" 16 | } 17 | ``` 18 | 19 | * Offsets can be expressed in either Amps or Watts. 20 | * Offsets can be either positive (count as consumption) or negative (count as generation) 21 | 22 | An example of how to call this function via cURL is: 23 | 24 | ``` 25 | curl -X POST -d '{ "offsetName": "WattsOffset", "offsetValue": 500.5, "offsetUnit": "W" } http://192.168.1.1:8080/api/addConsumptionOffset 26 | 27 | ``` 28 | 29 | This would instruct TWCManager to add or edit the offset described above. 30 | 31 | ## Integration with other platforms 32 | 33 | This API function can be called by external systems in order to control the charge rate of your TWC. This doesn't require any EMS modules or charging schedule, you can simply set your non-scheduled charging mode to Track Green Energy and then have the external system update an offset to either a negative (generation) or positive (consumption) value, and TWCManager will treat this value as if it were reported by an EMS and charge accordingly. 34 | 35 | ## Disabling an offset 36 | 37 | Set an offset's value to 0 (zero) to disable it. 38 | 39 | ## Deleting an offset 40 | 41 | See the [deleteConsumptionOffset](deleteConsumptionOffset.md) API call. 42 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/cancelChargeNow.md: -------------------------------------------------------------------------------- 1 | # cancelChargeNow API Command 2 | 3 | ## Introduction 4 | 5 | The cancelChargeNow API command allows you to instruct TWCManager to stop charging under the ChargeNow policy, and revert to the existing policy. 6 | 7 | ## Format of request 8 | 9 | The cancelChargeNow API command is not accompanied by any payload. You should send a blank payload when requesting this command. 10 | 11 | An example of how to call this function via cURL is: 12 | 13 | ``` 14 | curl -X POST -d "" http://192.168.1.1:8080/api/cancelChargeNow 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/chargeNow.md: -------------------------------------------------------------------------------- 1 | # chargeNow API Command 2 | 3 | ## Introduction 4 | 5 | The chargeNow API command allows you to instruct TWCManager to start charging at a given rate, for a given period of time. 6 | 7 | ## Format of request 8 | 9 | The chargeNow API command should be accompanied by a JSON-formatted request payload, specifying the rate to charge at and the time to charge for. The following is an example of a valid chargeNow payload: 10 | 11 | ``` 12 | { 13 | "chargeNowDuration": 3600, 14 | "chargeNowRate": 8 15 | } 16 | ``` 17 | 18 | This would instruct TWCManager to charge for 1 hour at 8A. 19 | 20 | An example of how to call this function via cURL is: 21 | 22 | ``` 23 | curl -X POST -d '{ "chargeNowRate": 8, "chargeNowDuration": 3600 }' http://192.168.1.1:8080/api/chargeNow 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/deleteConsumptionOffset.md: -------------------------------------------------------------------------------- 1 | # deleteConsumptionOffset API Command 2 | 3 | ## Introduction 4 | 5 | The deleteConsumptionOffset API command requests TWCManager to delete an existing consumption offset. 6 | 7 | ## Format of request 8 | 9 | The deleteConsumptionOffset API command is accompanied by a payload which describes the offset that you are deleting. The following example payload shows a request to delete an offset called WattsOffset: 10 | 11 | ``` 12 | { 13 | "offsetName": "WattsOffset", 14 | } 15 | ``` 16 | 17 | An example of how to call this function via cURL is: 18 | 19 | ``` 20 | curl -X POST -d '{ "offsetName": "WattsOffset" } http://192.168.1.1:8080/api/deleteConsumptionOffset 21 | 22 | ``` 23 | 24 | This would instruct TWCManager to delete the offset described above. 25 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/getConsumptionOffsets.md: -------------------------------------------------------------------------------- 1 | # getConsumptionOffsets API Command 2 | 3 | ## Introduction 4 | 5 | The getConsumptionOffsets API command requests TWCManager to provide a list of Consumption Offsets configured. 6 | 7 | ## Format of request 8 | 9 | The getConsumptionOffsets API command is not accompanied by any payload. You should send a blank payload when requesting this command. 10 | 11 | An example of how to call this function via cURL is: 12 | 13 | ``` 14 | curl -X GET -d "" http://192.168.1.1:8080/api/getConsumptionOffsets 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/saveSettings.md: -------------------------------------------------------------------------------- 1 | # saveSettings API Command 2 | 3 | ## Introduction 4 | 5 | The saveSettings API command instructs TWCManager to save the current settings dictionary stored in memory to disk. 6 | 7 | You could use this command after modifying settings (using the setSettings API call) if you would like those changes to be persistent. We don't automatically change settings after calling setSettings as this uses write cycles on flash disk, hence separation of the ability to call setSettings and to save these settings. 8 | 9 | ## Format of request 10 | 11 | The saveSettings API command is not accompanied by any payload. You should send a blank payload when requesting this command. 12 | 13 | An example of how to call this function via cURL is: 14 | 15 | ``` 16 | curl -X POST -d "" http://192.168.1.1:8080/api/saveSettings 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/sendStartCommand.md: -------------------------------------------------------------------------------- 1 | # sendStartCommand API Command 2 | 3 | ## Introduction 4 | 5 | The sendStartCommand API command instructs TWCManager to send a Start Charging message to all connected Slave TWCs. 6 | 7 | ## Format of request 8 | 9 | The sendStartCommand API command is not accompanied by any payload. You should send a blank payload when requesting this command. 10 | 11 | An example of how to call this function via cURL is: 12 | 13 | ``` 14 | curl -X POST -d "" http://192.168.1.1:8080/api/sendStartCommand 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/sendStopCommand.md: -------------------------------------------------------------------------------- 1 | # sendStopCommand API Command 2 | 3 | ## Introduction 4 | 5 | The sendStopCommand API command instructs TWCManager to send a stop command to all connected Slave TWCs. 6 | 7 | **Note**: The stop command sent will instruct all vehicles connected to immediately stop charging. They will not re-attempt charging if they are a Tesla vehicle until they are re-connected to the TWC. 8 | 9 | ## Format of request 10 | 11 | The sendStopCommand API command is not accompanied by any payload. You should send a blank payload when requesting this command. 12 | 13 | An example of how to call this function via cURL is: 14 | 15 | ``` 16 | curl -X POST -d "" http://192.168.1.1:8080/api/sendStopCommand 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/setScheduledChargingSettings.md: -------------------------------------------------------------------------------- 1 | # setScheduledChargingSettings API Command 2 | 3 | ## Introduction 4 | 5 | You can save the scheduled settings via a POST request 6 | 7 | ## Format of request 8 | 9 | The setScheduledChargingSettings API command should be accompanied by a JSON-formatted request payload, providing the data to set: 10 | 11 | | Attribute | Description | 12 | | --------------- | ------------------------------ | 13 | | **startingMinute** | Minutes from midnight scheduled charging time should start | 14 | | **endingMinute** | Minutes from midnight scheduled charging time should end | 15 | | **flexStartEnabled** | If you want to let your car starts charging at the end of Scheduled Charging Time, you can set this to true. This works only for exactly one TWCManager connected. Please be sure, that the amps provided are the power which your car can obtain. For better results set you battery size with flexBatterySize. | 16 | | **flexBatterySize** | Battery size of your car - default to 100 (biggest battery size - so your car should really finished before your ending time) | 17 | | **amps** | Charging Power you want to divide among the slaves. | 18 | 19 | 20 | ``` 21 | { 22 | "enabled": true, 23 | "startingMinute": 1260, 24 | "endingMinute": 420, 25 | "monday": true, 26 | "tuesday": true, 27 | "wednesday": true, 28 | "thursday": true, 29 | "friday": false, 30 | "saturday": false, 31 | "sunday": true, 32 | "amps": 20, 33 | "flexBatterySize": 100, 34 | "flexStartEnabled": true 35 | } 36 | ``` 37 | 38 | This would enable your car to start charging automatically at the end of the Scheduled Time - it calculates the needed time by the battery size (takes 92%) and adds a quarter at the end. Unless you want to charge more than 98% then it adds another half an hour. 39 | 40 | An example of how to call this function via cURL is: 41 | 42 | ``` 43 | curl -X POST -d '{ 44 | "enabled": true, 45 | "startingMinute": 1260, 46 | "endingMinute": 420, 47 | "monday": true, 48 | "tuesday": true, 49 | "wednesday": true, 50 | "thursday": true, 51 | "friday": false, 52 | "saturday": false, 53 | "sunday": true, 54 | "amps": 20, 55 | "flexBatterySize": 100, 56 | "flexStartEnabled": true 57 | }' http://192.168.1.1:8080/api/setScheduledChargingSettings 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/modules/control_HTTP_API/setSetting.md: -------------------------------------------------------------------------------- 1 | # setSetting API Command 2 | 3 | ## Introduction 4 | 5 | The setSetting API command provides an interface to directly specify TWCManager settings via the HTTP API. 6 | 7 | The command effectively allows the configuration of any setting that is stored in the TWCManager settings.json file. 8 | 9 | Please note that due to a lack of validation of input for this command, it is entirely possible to specify values which may cause TWCManager to behave in an unpredictable manner. It's always recommended to use dedicated API functions for controlling settings over direct settings manipulation where possible. 10 | 11 | ## Format of request 12 | 13 | The setSetting command can manipulate one setting at a time. The request consists of two values, setting and value. 14 | 15 | The following example shows how you could manipulate the Home Latitude value via the API. 16 | 17 | ``` 18 | { 19 | "setting": "homeLat", 20 | "value": 12345 21 | } 22 | ``` 23 | 24 | An example of how to call this function via cURL is: 25 | 26 | ``` 27 | curl -X POST -d '{ "setting": "homeLat", "value": 12345 }' http://192.168.1.1:8080/api/setSetting 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pyenv.md: -------------------------------------------------------------------------------- 1 | # Using pyenv to run additional Python interpreters 2 | 3 | ## Introduction 4 | 5 | If you have a Raspberry Pi build prior to late 2019, you may not have a version of Python interpreter install which is new enough to take advantage of some features which only support Python 3.6 or newer. 6 | 7 | If the output of the following comamnd: 8 | 9 | ``` 10 | python3 -V 11 | ``` 12 | 13 | Is any version number prior to 3.6.0, you might want to consider installing a newer Python intepreter. The following commands would install prerequisites for the pyenv platform, download and compible Python 3.7.1, and then run TWCManager under that python version. 14 | 15 | Keep in mind that compiling Python can take a long time, potentially hours on a Raspberry Pi. It is expected that the following commands would take considerable time to complete. 16 | 17 | ``` 18 | apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl git 19 | 20 | curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash 21 | 22 | pyenv install 3.7.1 23 | 24 | echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bashrc 25 | echo 'if [ -d "${PYENV_ROOT}" ]; then' >> ~/.bashrc 26 | echo ' export PATH=${PYENV_ROOT}/bin:$PATH' >> ~/.bashrc 27 | echo ' eval "$(pyenv init -)"' >> ~/.bashrc 28 | echo 'fi' >> ~/.bashrc 29 | 30 | exec $SHELL -l 31 | 32 | cd 33 | python3.7 setup.py install 34 | python3.7 -m TWCManager 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/rotary-switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/rotary-switch.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/screenshot.png -------------------------------------------------------------------------------- /docs/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/screenshot2.png -------------------------------------------------------------------------------- /docs/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/screenshot3.png -------------------------------------------------------------------------------- /docs/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/screenshot4.png -------------------------------------------------------------------------------- /docs/torxscrews.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/torxscrews.jpg -------------------------------------------------------------------------------- /docs/twccover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/twccover.png -------------------------------------------------------------------------------- /docs/twcinternalcover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/docs/twcinternalcover.png -------------------------------------------------------------------------------- /html/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/html/favicon.png -------------------------------------------------------------------------------- /html/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngardiner/TWCManager/68dbad2a6ece663e160d69e4359e6538146d35f0/html/refresh.png -------------------------------------------------------------------------------- /lib/TWCManager/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This files contains additional CSS settings 3 | * used for the TWC Manager 4 | */ 5 | 6 | span.VINClick { cursor: pointer; } 7 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/static/js/commonUI.js: -------------------------------------------------------------------------------- 1 | // Enable tooltips 2 | $(function () { 3 | $('[data-toggle="tooltip"]').tooltip() 4 | }) 5 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/static/js/debug.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#sendDebugCommand").click(function(e) { 3 | e.preventDefault(); 4 | $.ajax({ 5 | type: "POST", 6 | url: "/api/sendDebugCommand", 7 | data: JSON.stringify({ 8 | commandName: $("#cmdDropdown").val(), 9 | customCommand: $("#cmdCustom").val() 10 | }), 11 | }); 12 | setTimeout(function(){ 13 | var lookup = $.ajax({ 14 | type: "GET", 15 | url: "/api/getLastTWCResponse", 16 | data: {} 17 | }) 18 | lookup.done(function(html) { 19 | $("#lastTWCResponse").val(html); 20 | }); 21 | }, 3000); 22 | }); 23 | 24 | $("#TeslaAPICommand").change(function () { 25 | 26 | if (this.value == "setChargeRate") { 27 | $("#TeslaAPIParams").val('{ "charge_rate": 5 }'); 28 | } 29 | if (this.value == "wakeVehicle") { 30 | $("#TeslaAPIParams").val("{}"); 31 | } 32 | }); 33 | 34 | $("#sendTeslaAPICommand").click(function(e) { 35 | e.preventDefault(); 36 | $.ajax({ 37 | type: "POST", 38 | url: "/api/sendTeslaAPICommand", 39 | data: JSON.stringify({ 40 | commandName: $("#TeslaAPICommand").val(), 41 | parameters: $("#TeslaAPIParams").val(), 42 | vehicleID: $("#TeslaAPIVehicle").val() 43 | }), 44 | }); 45 | }); 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/static/js/settings.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#addOffset").click(function(e) { 3 | e.preventDefault(); 4 | $.ajax({ 5 | type: "POST", 6 | url: "/api/addConsumptionOffset", 7 | data: JSON.stringify({ 8 | offsetName: $("#offsetName").val(), 9 | offsetValue: $("#offsetValue").val(), 10 | offsetUnit: $("#offsetUnit").val() 11 | }), 12 | dataType: "json" 13 | }); 14 | }); 15 | }); 16 | 17 | // Draw the buttons which appear in the Action column next to each entry in the Consumption Offset 18 | // table 19 | function offsetActionButtons(key, value, unit) { 20 | var actionButtons = ""; 21 | actionButtons += ""; 22 | actionButtons += "   "; 23 | actionButtons += ""; 24 | actionButtons += ""; 25 | return actionButtons; 26 | } 27 | 28 | function deleteOffset(key) { 29 | $.ajax({ 30 | type: "POST", 31 | url: "/api/deleteConsumptionOffset", 32 | data: JSON.stringify({ 33 | offsetName: key 34 | }), 35 | dataType: "json" 36 | }); 37 | } 38 | 39 | function editOffset(key, value, unit) { 40 | $("#offsetName").val(key) 41 | $("#offsetValue").val(value) 42 | $("#offsetUnit").val(unit) 43 | } 44 | 45 | // AJAJ refresh for getConsumptionOffset call 46 | $(document).ready(function() { 47 | function getConsumptionOffset() { 48 | $.ajax({ 49 | url: "/api/getConsumptionOffsets", 50 | dataType: "text", 51 | cache: false, 52 | success: function(data) { 53 | var json = $.parseJSON(data); 54 | $("#consumptionOffsets tbody").empty(); 55 | Object.keys(json).sort().forEach(function(key) { 56 | $('#consumptionOffsets tbody').append(""+key+""+json[key]['value']+ " " + json[key]["unit"] + "" + offsetActionButtons(key, json[key]['value'], json[key]["unit"]) + ""); 57 | }); 58 | } 59 | }); 60 | setTimeout(getConsumptionOffset, 2000); 61 | } 62 | 63 | getConsumptionOffset(); 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/static/js/status.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | $("#cancel_chargenow").click(function(e) { 4 | e.preventDefault(); 5 | $.ajax({ 6 | type: "POST", 7 | url: "/api/cancelChargeNow", 8 | data: {} 9 | }); 10 | }); 11 | }); 12 | 13 | $(document).ready(function() { 14 | $("#send_start_command").click(function(e) { 15 | e.preventDefault(); 16 | $.ajax({ 17 | type: "POST", 18 | url: "/api/sendStartCommand", 19 | data: {} 20 | }); 21 | }); 22 | }); 23 | 24 | $(document).ready(function() { 25 | $("#send_stop_command").click(function(e) { 26 | e.preventDefault(); 27 | $.ajax({ 28 | type: "POST", 29 | url: "/api/sendStopCommand", 30 | data: {} 31 | }); 32 | }); 33 | }); 34 | 35 | function loadVIN(twc, vin) { 36 | window.open("/vehicleDetails/" + document.getElementById(twc+"_"+vin+"VIN").innerHTML, '_blank'); 37 | } 38 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/bootstrap.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/drawChart.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 111 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/graphs.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TWCManager 5 | {% include 'bootstrap.html.j2' %} 6 | {% include 'drawChart.html.j2' %} 7 | 8 | 9 | {% include 'navbar.html.j2' %} 10 | {% if master.checkModuleCapability("Logging", "queryGreenEnergy") %} 11 |
12 |

Initial Date: End Date: 13 | 14 | 15 | 16 |

17 | 18 |
19 | {% else %} 20 | Note: Graphs require the use of a Logging module which supports querying of saved data. Currently this is: MySQL 21 | {% endif %} 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/handle_teslalogin.html.j2: -------------------------------------------------------------------------------- 1 | {% if url.path == "/teslaAccount/unknown" %} 2 | 3 | Sorry, an unknown error has occurred when trying to fetch your tokens. 4 | 5 | {% elif url.path == "/teslaAccount/invalid_grant" %} 6 | 7 | Tesla API reports that the grant provided is invalid. This usually happens if you use an older URL response and restart TWCManager. Please perform the login process again. 8 | 9 | {% elif url.path == "/teslaAccount/response_no_token" %} 10 | 11 | The Tesla API did not provide an access token in the token response. 12 | 13 | {% elif url.path == "/teslaAccount/success" %} 14 | 15 | Success, your Tesla API tokens have been stored. 16 | 17 | {% endif %} 18 | 19 | {% if not master.teslaLoginAskLater 20 | and not master.tokenSyncEnabled() 21 | and url.path != "/teslaAccount/success" %} 22 | 24 | {% if not apiAvailable %} 25 | {% include 'request_teslalogin.html.j2' %} 26 | {% endif %} 27 | {% endif %} 28 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/jsrefresh.html.j2: -------------------------------------------------------------------------------- 1 | 78 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/main.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TWCManager 5 | {% include 'bootstrap.html.j2' %} 6 | {% include 'jsrefresh.html.j2' %} 7 | 8 | 9 | 10 | {% include 'navbar.html.j2' %} 11 | 12 | 13 | 23 | 26 | 27 |
14 | {% include 'handle_teslalogin.html.j2' %} 15 | {% if master.lastSaveFailed %} 16 | 17 | We experienced an error trying to save your settings. This means that while your current settings will be retained for the current session, they will not be saved if TWCManager exits. Please check the permissions on /etc/twcmanager/settings.json and try saving your settings again. 18 | 19 | {% endif %} 20 | 21 | {% include 'showStatus.html.j2' %} 22 | 24 | {{ doChargeSchedule()|safe }} 25 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/navbar.html.j2: -------------------------------------------------------------------------------- 1 | 26 |
27 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/policy.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TWCManager 5 | {% include 'bootstrap.html.j2' %} 6 | 7 | 8 | {% include 'navbar.html.j2' %} 9 | 10 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/request_teslalogin.html.j2: -------------------------------------------------------------------------------- 1 |
2 |

TWCManager uses Tesla API to start and stop Tesla vehicles you own from charging. 3 |

4 | 5 | 6 |

7 | 8 | To log in to your Tesla account, please do the following: 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 34 | 35 |
Step 1Click the following link: Tesla Login
Step 2After you login, you'll be redirected to a Page Not Found page with an auth.tesla.com URL. Copy the URL from your browser bar.
Paste the URL here:
23 | {{ addButton( 24 | ["later", "Save API Key"], 25 | "class='btn btn-outline-info' name='save'", 26 | )|safe }} 27 | 29 | {{ addButton( 30 | ["later", "Ask Me Later"], 31 | "class='btn btn-outline-info' name='later'", 32 | )|safe }} 33 |
36 |

37 |
38 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/schedule.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TWCManager 5 | {% include 'bootstrap.html.j2' %} 6 | 7 | 8 | {% include 'navbar.html.j2' %} 9 |
10 | 11 | 12 | 74 | 85 | 86 |
87 | This scheduling interface is currently in compatibility mode to make it compatible with existing scheduling settings. For that reason, whilst you may set start and stop times to the minute, only the hour will currently apply. The ability to set scheduled hours per day is also currently disabled. 88 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/showCommands.html.j2: -------------------------------------------------------------------------------- 1 |
13 | 14 | 15 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 71 | 72 |
Charge Schedule Settings: 16 |
Resume tracking green energy at: 20 | {{ optionList(timeList, 21 | { 22 | "name": "resumeGreenEnergy", 23 | "value": master.settings.get("Schedule", {}).get("Settings", {}).get("resumeGreenEnergy", "00:00"), 24 | })|safe 25 | }} 26 | Green Energy Tracking will not start until this time each day.
Scheduled Charge Rate: 32 | {{ optionList( 33 | ampsList, 34 | { 35 | "name": "scheduledAmpsMax", 36 | "value": master.settings.get("Schedule", {}).get("Settings", {}).get("scheduledAmpsMax", "0"), 37 | }, 38 | )|safe 39 | }} 40 |
Scheduled Charge Time: Specify Charge Time per Day 45 |
  Same Charge Time for all scheduled days: 49 |  
  54 | {{ optionList(timeList, 55 | {"name": "startCommonChargeTime", 56 | "value": master.settings.get("Schedule", {}).get("Common", {}).get("start", "00:00")})|safe }} 57 | to 60 | {{ optionList(timeList, 61 | {"name": "endCommonChargeTime", 62 | "value": master.settings.get("Schedule", {}).get("Common", {}).get("end", "00:00")})| safe }} 63 |  
Click here for more information on Charge Scheduling. 70 |
73 |
75 | 76 | 77 | 79 | {% for dayOfWeek in ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] %} 80 | {{ chargeScheduleDay(dayOfWeek)|safe }} 81 | {% endfor %} 82 |
Charging Schedule: 78 |
83 | 84 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 21 | 22 | 23 | 29 | 34 | 40 | 41 |
Charge NowCommands
Charge for: 9 | {{ optionList(hoursDurationList, {"name": "chargeNowDuration"})|safe }} 10 | Charge Rate: 13 | {{ optionList(ampsList[1:], {"name": "chargeNowRate"})|safe }} 14 | 16 | {{ addButton( 17 | ["send_stop_command", "Stop All Charging"], 18 | "class='btn btn-outline-danger' data-toggle='tooltip' data-placement='top' title='WARNING: This function causes Tesla Vehicles to Stop Charging until they are physically re-connected to the TWC.'", 19 | )|safe }} 20 |
24 | {{ addButton( 25 | ["start_chargenow", "Start Charge Now"], 26 | "class='btn btn-outline-success' data-toggle='tooltip' data-placement='top'", 27 | )|safe }} 28 | 30 | {{ addButton( 31 | ["cancel_chargenow", "Cancel Charge Now"], "class='btn btn-outline-danger'" 32 | )|safe }} 33 | 35 | {{ addButton( 36 | ["send_start_command", "Start All Charging"], 37 | "class='btn btn-outline-success'", 38 | )|safe }} 39 |
42 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/upgrade.html.j2: -------------------------------------------------------------------------------- 1 |

Attempting Upgrade

2 | 3 |
4 | 


--------------------------------------------------------------------------------
/lib/TWCManager/Control/themes/Default/upgradePrompt.html.j2:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     TWCManager
 5 |     {% include 'bootstrap.html.j2' %}
 6 |     
 7 |   
 8 |   
 9 |     {% include 'navbar.html.j2' %}
10 | 
11 |    

If you choose to continue, TWCManager will attempt to upgrade itself. 12 | 13 | This requires that TWCManager has write access to the twcmanager user account's home directory. 14 | 15 | Once the upgrade is complete you will need to restart TWCManager (either via docker, systemctl or manually) for the new version to take effect.

16 | 17 |

18 | If you're sure that you would like to continue, please click the link below: 19 |

20 | 21 |

22 | Upgrade TWCManager 23 | or 24 | Cancel Upgrade 25 |

26 | 27 |

28 | Note: it is normal for your browser to not immediately show output on clicking the Upgrade button above. 29 |

30 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Default/vehicles.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TWCManager 5 | {% include 'bootstrap.html.j2' %} 6 | 7 | 8 | {% include 'navbar.html.j2' %} 9 |

 

10 |

Vehicles

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% if master.settings["Vehicles"] %} 20 | {% for vehicle in master.settings["Vehicles"].keys()|sort %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | {% endif %} 30 |
Vehicle VIN Charges Total kWh
{{ vehicle }} {{ master.settings["Vehicles"][vehicle]["chargeSessions"] }} {{ master.settings["Vehicles"][vehicle]["totalkWh"] }}
31 | 32 |

 

33 |

Vehicle Groups

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% for group in master.settings["VehicleGroups"].keys()|sort %} 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 63 | 64 | {% endfor %} 65 |
Group Name Members Description Actions
{{ group }}  49 | {% for vehicle in master.settings["VehicleGroups"][group]["Members"] %} 50 | {{ vehicle }}
51 | {% endfor %} 52 |
 {{ master.settings["VehicleGroups"][group]["Description"] }}  57 | {% if master.settings["VehicleGroups"][group]["Built-in"] %} 58 | Delete 59 | {% else %} 60 | Delete 61 | {% endif %} 62 |
66 | 67 |

 

68 |
Add Group
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
Group Name   Description   
82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Modern/bootstrap.js.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Modern/css/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This files contains additional CSS settings 3 | * used for the TWC Manager 4 | */ 5 | .display-4 { 6 | font-weight: 200; 7 | } 8 | 9 | .display-5 { 10 | font-size: 1rem; 11 | font-weight: 500; 12 | } 13 | 14 | .meter { 15 | font-size: 1.0rem; 16 | font-weight: 300; 17 | line-height: 1.2; 18 | margin-bottom: 0; 19 | } 20 | 21 | .amps::after { 22 | margin-left: 0.3rem; 23 | content: "A"; 24 | color: #6c757d; // .text-secondary color 25 | } 26 | 27 | .kwh::after { 28 | margin-left: 0.4rem; 29 | content: "kWh"; 30 | color: #6c757d; // .text-secondary color 31 | } 32 | 33 | .volt { 34 | margin-left: 0.2rem; 35 | } 36 | 37 | .volt::after { 38 | margin-left: 0.15rem; 39 | content: "V"; 40 | color: #6c757d; 41 | } 42 | 43 | .car { 44 | stroke: #6c757d; 45 | fill: #6c757d; 46 | } 47 | 48 | .phase { 49 | position: relative; 50 | font-size: 75%; 51 | bottom: -0.4em; 52 | margin-left: 2px; 53 | color: #6c757d; 54 | } 55 | 56 | .display-watt { 57 | margin-top: -6px; 58 | } 59 | 60 | .badge-grey { 61 | background-color: #dfdfdf; 62 | } 63 | 64 | .rounded-xl { 65 | border-radius: 1rem !important; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /lib/TWCManager/Control/themes/Modern/master.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "bootstrap.html.j2" %} 5 | {% block pagetitle %}{% endblock %} 6 | 7 | 8 | 9 | {% include "navbar.html.j2" %} 10 |
11 | {% block content %}{% endblock %} 12 |
13 | {% include "bootstrap.js.html.j2" %} 14 | {% block javascripts %}{% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/TWCManager/EMS/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /lib/TWCManager/EMS/DSMR.py: -------------------------------------------------------------------------------- 1 | # Dutch SmartMeter Serial Integration (DSMR) 2 | 3 | 4 | class DSMR: 5 | import time 6 | 7 | baudrate = 115200 8 | consumedW = 0 9 | generatedW = 0 10 | serial = None 11 | serialPort = "/dev/ttyUSB2" 12 | status = False 13 | timeout = 0 14 | voltage = 0 15 | 16 | def __init__(self, config): 17 | self.baudrate = config.get("baudrate", "115200") 18 | self.status = config.get("enabled", False) 19 | self.serialPort = config.get("serialPort", "/dev/ttyUSB2") 20 | 21 | # Unload if this module is disabled or misconfigured 22 | if (not self.status) or (not self.serialPort) or (int(self.baudRate) < 1): 23 | self.master.releaseModule("lib.TWCManager.EMS", "Fronius") 24 | return None 25 | 26 | def main(self): 27 | self.serial.port = self.serialPort 28 | try: 29 | self.serial.open() 30 | except ValueError: 31 | sys.exit("Error opening serial port (%s). exiting" % self.serial.name) 32 | -------------------------------------------------------------------------------- /lib/TWCManager/EMS/Efergy.py: -------------------------------------------------------------------------------- 1 | # Efergy 2 | import time 3 | import logging 4 | 5 | logger = logging.getLogger("\U000026a1 Efergy") 6 | 7 | 8 | class Efergy: 9 | import requests 10 | 11 | cacheTime = 10 12 | config = None 13 | configConfig = None 14 | configEfergy = None 15 | consumedW = 0 16 | debugLevel = 0 17 | fetchFailed = False 18 | token = 0 19 | generatedW = 0 20 | importW = 0 21 | exportW = 0 22 | lastFetch = 0 23 | master = None 24 | status = False 25 | timeout = 10 26 | voltage = 0 27 | 28 | def __init__(self, master): 29 | self.master = master 30 | self.config = master.config 31 | try: 32 | self.configConfig = master.config["config"] 33 | except KeyError: 34 | self.configConfig = {} 35 | try: 36 | self.configEfergy = master.config["sources"]["Efergy"] 37 | except KeyError: 38 | self.configEfergy = {} 39 | self.debugLevel = self.configConfig.get("debugLevel", 0) 40 | self.status = self.configEfergy.get("enabled", False) 41 | self.token = self.configEfergy.get("token", None) 42 | 43 | # Unload if this module is disabled or misconfigured 44 | if not self.status: 45 | self.master.releaseModule("lib.TWCManager.EMS", self.__class__.__name__) 46 | return None 47 | 48 | def getConsumption(self): 49 | if not self.status: 50 | logger.debug("Efergy EMS Module Disabled. Skipping getConsumption") 51 | return 0 52 | 53 | # Perform updates if necessary 54 | self.update() 55 | 56 | # Return consumption value 57 | return float(self.consumedW) 58 | 59 | def getGeneration(self): 60 | if not self.status: 61 | logger.debug("Efergy EMS Module Disabled. Skipping getGeneration") 62 | return 0 63 | 64 | # Perform updates if necessary 65 | self.update() 66 | 67 | # Return generation value 68 | if not self.generatedW: 69 | self.generatedW = 0 70 | return float(self.generatedW) 71 | 72 | def getValue(self, url): 73 | # Fetch the specified URL from the Efergy and return the data 74 | self.fetchFailed = False 75 | 76 | try: 77 | r = self.requests.get(url, timeout=self.timeout) 78 | except self.requests.exceptions.ConnectionError as e: 79 | logger.log( 80 | logging.INFO4, "Error connecting to Efergy to fetch sensor value" 81 | ) 82 | logger.debug(str(e)) 83 | self.fetchFailed = True 84 | return False 85 | 86 | r.raise_for_status() 87 | jsondata = r.json() 88 | return jsondata 89 | 90 | def getMeterData(self): 91 | url = ( 92 | "https://engage.efergy.com/mobile_proxy/getCurrentValuesSummary?token=" 93 | + self.token 94 | ) 95 | 96 | return self.getValue(url) 97 | 98 | def update(self): 99 | if (int(time.time()) - self.lastFetch) > self.cacheTime: 100 | # Cache has expired. Fetch values from Efergy. 101 | 102 | meterData = self.getMeterData() 103 | 104 | if meterData: 105 | try: 106 | self.consumedW = list(meterData[0]["data"][0].values())[0] 107 | except (KeyError, TypeError) as e: 108 | logger.log( 109 | logging.INFO4, 110 | "Exception during parsing Meter Data (Consumption)", 111 | ) 112 | logger.debug(str(e)) 113 | 114 | # Update last fetch time 115 | if self.fetchFailed is not True: 116 | self.lastFetch = int(time.time()) 117 | 118 | return True 119 | else: 120 | # Cache time has not elapsed since last fetch, serve from cache. 121 | return False 122 | -------------------------------------------------------------------------------- /lib/TWCManager/EMS/TED.py: -------------------------------------------------------------------------------- 1 | # The Energy Detective (TED) 2 | import logging 3 | import re 4 | import requests 5 | import time 6 | 7 | 8 | logger = logging.getLogger("\U000026c5 TED") 9 | 10 | 11 | class TED: 12 | # I check solar panel generation using an API exposed by The 13 | # Energy Detective (TED). It's a piece of hardware available 14 | # at http://www.theenergydetective.com 15 | 16 | cacheTime = 10 17 | config = None 18 | configConfig = None 19 | configTED = None 20 | consumedW = 0 21 | fetchFailed = False 22 | generatedW = 0 23 | importW = 0 24 | exportW = 0 25 | lastFetch = 0 26 | master = None 27 | serverIP = None 28 | serverPort = 80 29 | status = False 30 | timeout = 10 31 | voltage = 0 32 | 33 | def __init__(self, master): 34 | self.master = master 35 | self.config = master.config 36 | try: 37 | self.configConfig = self.config["config"] 38 | except KeyError: 39 | self.configConfig = {} 40 | try: 41 | self.configTED = self.config["sources"]["TED"] 42 | except KeyError: 43 | self.configTED = {} 44 | self.status = self.configTED.get("enabled", False) 45 | self.serverIP = self.configTED.get("serverIP", None) 46 | self.serverPort = self.configTED.get("serverPort", "80") 47 | 48 | # Unload if this module is disabled or misconfigured 49 | if (not self.status) or (not self.serverIP) or (int(self.serverPort) < 1): 50 | self.master.releaseModule("lib.TWCManager.EMS", "TED") 51 | return None 52 | 53 | def getConsumption(self): 54 | if not self.status: 55 | logger.debug("TED EMS Module Disabled. Skipping getConsumption") 56 | return 0 57 | 58 | # Perform updates if necessary 59 | self.update() 60 | 61 | # I don't believe this is implemented 62 | return float(0) 63 | 64 | def getGeneration(self): 65 | if not self.status: 66 | logger.debug("TED EMS Module Disabled. Skipping getGeneration") 67 | return 0 68 | 69 | # Perform updates if necessary 70 | self.update() 71 | 72 | # Return generation value 73 | return float(self.generatedW) 74 | 75 | def getTEDValue(self, url): 76 | # Fetch the specified URL from TED and return the data 77 | self.fetchFailed = False 78 | 79 | try: 80 | r = requests.get(url, timeout=self.timeout) 81 | except requests.exceptions.ConnectionError as e: 82 | logger.log(logging.INFO4, "Error connecting to TED to fetch solar data") 83 | logger.debug(str(e)) 84 | self.fetchFailed = True 85 | return False 86 | 87 | r.raise_for_status() 88 | return r 89 | 90 | def update(self): 91 | if (int(time.time()) - self.lastFetch) > self.cacheTime: 92 | # Cache has expired. Fetch values from HomeAssistant sensor. 93 | 94 | url = "http://" + self.serverIP + ":" + self.serverPort 95 | url = url + "/history/export.csv?T=1&D=0&M=1&C=1" 96 | 97 | value = self.getTEDValue(url) 98 | m = None 99 | if value: 100 | m = re.search(b"^Solar,[^,]+,-?([^, ]+),", value, re.MULTILINE) 101 | else: 102 | logger.log(logging.INFO5, "Failed to find value in response from TED") 103 | self.fetchFailed = True 104 | 105 | if m: 106 | self.generatedW = int(float(m.group(1)) * 1000) 107 | 108 | # Update last fetch time 109 | if self.fetchFailed is not True: 110 | self.lastFetch = int(time.time()) 111 | 112 | return True 113 | else: 114 | # Cache time has not elapsed since last fetch, serve from cache. 115 | return False 116 | -------------------------------------------------------------------------------- /lib/TWCManager/Interface/Dummy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | logger = logging.getLogger(__name__.rsplit(".")[-1]) 5 | 6 | 7 | class Dummy: 8 | enabled = False 9 | master = None 10 | msgBuffer = bytes() 11 | proto = None 12 | twcID = bytearray(b"\x12\x34") 13 | timeLastTx = 0 14 | 15 | def __init__(self, master): 16 | self.master = master 17 | classname = self.__class__.__name__ 18 | 19 | # Unload if this module is disabled or misconfigured 20 | if "interface" in master.config and classname in master.config["interface"]: 21 | self.enabled = master.config["interface"][classname].get("enabled", True) 22 | if not self.enabled: 23 | self.master.releaseModule("lib.TWCManager.Interface", classname) 24 | return None 25 | 26 | # Configure the module 27 | if "interface" in master.config: 28 | if master.config["interface"][classname].get("twcID", False): 29 | self.twcID = bytearray( 30 | str(master.config["interface"][classname].get("twcID")).encode() 31 | ) 32 | 33 | # Instantiate protocol module for sending/recieving TWC protocol 34 | self.proto = self.master.getModuleByName("TWCProtocol") 35 | 36 | def close(self): 37 | # NOOP - No need to close anything 38 | return 0 39 | 40 | def getBufferLen(self): 41 | # This function returns the size of the recieve buffer. 42 | # This is used by read functions to determine if information is waiting 43 | return len(self.msgBuffer) 44 | 45 | def send(self, msg): 46 | # This is the external send interface - it is called by TWCManager which expects that it is 47 | # talking to a live TWC. The key here is that we treat it as our reciept interface and parse 48 | # the message as if we are a TWC 49 | 50 | packet = self.proto.parseMessage(msg) 51 | if packet["Command"] == "MasterLinkready2": 52 | self.sendInternal( 53 | self.proto.createMessage( 54 | { 55 | "Command": "SlaveLinkready", 56 | "SenderID": self.twcID, 57 | "Sign": self.master.getSlaveSign(), 58 | "Amps": bytearray(b"\x1f\x40"), 59 | } 60 | ) 61 | ) 62 | elif packet["Command"] == "MasterHeartbeat": 63 | self.sendInternal( 64 | self.proto.createMessage( 65 | { 66 | "Command": "SlaveHeartbeat", 67 | "SenderID": self.twcID, 68 | "RecieverID": packet["SenderID"], 69 | } 70 | ) 71 | ) 72 | 73 | logger.log(logging.INFO9, "Tx@: " + self.master.hex_str(msg)) 74 | self.timeLastTx = time.time() 75 | return 0 76 | 77 | def read(self, len): 78 | # Read our buffered messages. We simulate this by making a copy of the 79 | # current message buffer, clearing the read message buffer bytes and then 80 | # returning the copied message to TWCManager. This is what it would look 81 | # like if we read from a serial interface 82 | localMsgBuffer = self.msgBuffer[:len] 83 | self.msgBuffer = self.msgBuffer[len:] 84 | return localMsgBuffer 85 | 86 | def sendInternal(self, msg): 87 | # The sendInternal function takes a message that we would like to send 88 | # from the dummy module to the TWCManager, adds the required checksum, 89 | # updates the internal message buffer with the sent message and then 90 | # allows this to be polled & read by TWCManager on the next loop iteration 91 | 92 | msg = bytearray(msg) 93 | checksum = 0 94 | for i in range(1, len(msg)): 95 | checksum += msg[i] 96 | 97 | msg.append(checksum & 0xFF) 98 | 99 | # Escaping special chars: 100 | # The protocol uses C0 to mark the start and end of the message. If a C0 101 | # must appear within the message, it is 'escaped' by replacing it with 102 | # DB and DC bytes. 103 | # A DB byte in the message is escaped by replacing it with DB DD. 104 | # 105 | # User FuzzyLogic found that this method of escaping and marking the start 106 | # and end of messages is based on the SLIP protocol discussed here: 107 | # https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol 108 | 109 | i = 0 110 | while i < len(msg): 111 | if msg[i] == 0xC0: 112 | msg[i : i + 1] = b"\xdb\xdc" 113 | i = i + 1 114 | elif msg[i] == 0xDB: 115 | msg[i : i + 1] = b"\xdb\xdd" 116 | i = i + 1 117 | i = i + 1 118 | 119 | msg = bytearray(b"\xc0" + msg + b"\xc0\xfe") 120 | logger.log(logging.INFO9, "TxInt@: " + self.master.hex_str(msg)) 121 | 122 | self.msgBuffer = msg 123 | -------------------------------------------------------------------------------- /lib/TWCManager/Interface/RS485.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | logger = logging.getLogger("\U0001f50c RS485") 5 | 6 | 7 | class RS485: 8 | import serial 9 | 10 | baud = 9600 11 | enabled = True 12 | master = None 13 | port = None 14 | ser = None 15 | timeLastTx = 0 16 | 17 | def __init__(self, master): 18 | self.master = master 19 | classname = self.__class__.__name__ 20 | 21 | # Unload if this module is disabled or misconfigured 22 | if "interface" in master.config and classname in master.config["interface"]: 23 | self.enabled = master.config["interface"][classname].get("enabled", True) 24 | if not self.enabled: 25 | self.master.releaseModule("lib.TWCManager.Interface", classname) 26 | return None 27 | 28 | # There are two places that the baud rate for the RS485 adapter may be stored. 29 | # The first is the legacy configuration path, and the second is the new 30 | # dedicated interface configuration. We check either/both for this value 31 | bauda = master.config["config"].get("baud", 0) 32 | baudb = None 33 | if "interface" in master.config: 34 | baudb = master.config["interface"]["RS485"].get("baud", 0) 35 | if baudb: 36 | self.baud = baudb 37 | elif bauda: 38 | self.baud = bauda 39 | 40 | # Similarly, there are two places to check for a port defined. 41 | porta = master.config["config"].get("rs485adapter", "") 42 | portb = None 43 | if "interface" in master.config: 44 | portb = master.config["interface"]["RS485"].get("port", "") 45 | if portb: 46 | self.port = portb 47 | elif porta: 48 | self.port = porta 49 | 50 | self.connect() 51 | 52 | def connect(self): 53 | # Reset any Slave TWC last RX heartbeat counters in case serial reconnection has occurred 54 | for slaveTWC in self.master.getSlaveTWCs(): 55 | slaveTWC.timeLastRx = time.time() 56 | 57 | # Connect to serial port 58 | self.ser = self.serial.serial_for_url(self.port, self.baud, timeout=0) 59 | 60 | def close(self): 61 | # Close the serial interface 62 | return self.ser.close() 63 | 64 | def getBufferLen(self): 65 | # This function returns the size of the recieve buffer. 66 | # This is used by read functions to determine if information is waiting 67 | return self.ser.inWaiting() 68 | 69 | def read(self, len): 70 | # Read the specified amount of data from the serial interface 71 | try: 72 | return self.ser.read(len) 73 | except serial.serialutil.SerialException as e: 74 | logger.log( 75 | logging.ERROR, 76 | "Error reading from serial interface: {}. Will attempt re-connect.".format( 77 | e 78 | ), 79 | ) 80 | self.connect() 81 | 82 | def send(self, msg): 83 | # Send msg on the RS485 network. We'll escape bytes with a special meaning, 84 | # add a CRC byte to the message end, and add a C0 byte to the start and end 85 | # to mark where it begins and ends. 86 | 87 | msg = bytearray(msg) 88 | checksum = 0 89 | for i in range(1, len(msg)): 90 | checksum += msg[i] 91 | 92 | msg.append(checksum & 0xFF) 93 | 94 | # Escaping special chars: 95 | # The protocol uses C0 to mark the start and end of the message. If a C0 96 | # must appear within the message, it is 'escaped' by replacing it with 97 | # DB and DC bytes. 98 | # A DB byte in the message is escaped by replacing it with DB DD. 99 | # 100 | # User FuzzyLogic found that this method of escaping and marking the start 101 | # and end of messages is based on the SLIP protocol discussed here: 102 | # https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol 103 | 104 | i = 0 105 | while i < len(msg): 106 | if msg[i] == 0xC0: 107 | msg[i : i + 1] = b"\xdb\xdc" 108 | i = i + 1 109 | elif msg[i] == 0xDB: 110 | msg[i : i + 1] = b"\xdb\xdd" 111 | i = i + 1 112 | i = i + 1 113 | 114 | msg = bytearray(b"\xc0" + msg + b"\xc0") 115 | logger.log(logging.INFO9, "Tx@: " + self.master.hex_str(msg)) 116 | 117 | self.ser.write(msg) 118 | 119 | self.timeLastTx = time.time() 120 | -------------------------------------------------------------------------------- /lib/TWCManager/Interface/TCP.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | 4 | 5 | logger = logging.getLogger(__name__.rsplit(".")[-1]) 6 | 7 | 8 | class TCP: 9 | import time 10 | 11 | config = None 12 | configTCP = None 13 | enabled = False 14 | master = None 15 | port = 6000 16 | server = None 17 | sock = None 18 | timeLastTx = 0 19 | 20 | def __init__(self, master): 21 | self.master = master 22 | self.config = master.config 23 | if "interface" in master.config: 24 | self.configTCP = master.config["interface"].get("TCP", {}) 25 | else: 26 | self.configTCP = {} 27 | 28 | self.enabled = self.configTCP.get("enabled", False) 29 | # Unload if this module is disabled or misconfigured 30 | if not self.enabled: 31 | self.master.releaseModule("lib.TWCManager.Interface", "TCP") 32 | return None 33 | 34 | # Create TCP socket 35 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 36 | 37 | # If we are configured to listen, open the listening socket 38 | if self.configTCP.get("listen", False): 39 | self.sock.bind(("localhost", self.port)) 40 | self.sock.listen(1) 41 | else: 42 | # Connect to server 43 | self.sock.connect((self.server, self.port)) 44 | 45 | def close(self): 46 | # Close the TCP socket interface 47 | self.sock.close() 48 | 49 | def getBufferLen(self): 50 | # This function returns the size of the recieve buffer. 51 | # This is used by read functions to determine if information is waiting 52 | return 0 53 | 54 | def read(self, len): 55 | # Read the specified amount of data from the TCP interface 56 | return self.sock.recv(len) 57 | 58 | def send(self, msg): 59 | # Send msg on the RS485 network. We'll escape bytes with a special meaning, 60 | # add a CRC byte to the message end, and add a C0 byte to the start and end 61 | # to mark where it begins and ends. 62 | 63 | msg = bytearray(msg) 64 | checksum = 0 65 | for i in range(1, len(msg)): 66 | checksum += msg[i] 67 | 68 | msg.append(checksum & 0xFF) 69 | 70 | # Escaping special chars: 71 | # The protocol uses C0 to mark the start and end of the message. If a C0 72 | # must appear within the message, it is 'escaped' by replacing it with 73 | # DB and DC bytes. 74 | # A DB byte in the message is escaped by replacing it with DB DD. 75 | # 76 | # User FuzzyLogic found that this method of escaping and marking the start 77 | # and end of messages is based on the SLIP protocol discussed here: 78 | # https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol 79 | 80 | i = 0 81 | while i < len(msg): 82 | if msg[i] == 0xC0: 83 | msg[i : i + 1] = b"\xdb\xdc" 84 | i = i + 1 85 | elif msg[i] == 0xDB: 86 | msg[i : i + 1] = b"\xdb\xdd" 87 | i = i + 1 88 | i = i + 1 89 | 90 | msg = bytearray(b"\xc0" + msg + b"\xc0") 91 | logger.log(logging.INFO9, "Tx@: " + self.master.hex_str(msg)) 92 | 93 | self.sock.send(msg) 94 | 95 | self.timeLastTx = self.time.time() 96 | -------------------------------------------------------------------------------- /lib/TWCManager/Logging/ConsoleLogging.py: -------------------------------------------------------------------------------- 1 | # ConsoleLogging module. Provides output to console for logging. 2 | import logging 3 | 4 | import sys 5 | from termcolor import colored 6 | 7 | 8 | logger = logging.getLogger(__name__.rsplit(".")[-1]) 9 | 10 | 11 | class ColorFormatter(logging.Formatter): 12 | def format(self, record): 13 | if hasattr(record, "colored"): 14 | old_args = record.args 15 | record.args = tuple(colored(arg, record.colored) for arg in record.args) 16 | s = super(ColorFormatter, self).format(record) 17 | record.args = old_args 18 | else: 19 | s = super(ColorFormatter, self).format(record) 20 | return s 21 | 22 | 23 | class ConsoleLogging: 24 | capabilities = {"queryGreenEnergy": False} 25 | config = None 26 | configConfig = None 27 | configLogging = None 28 | status = True 29 | 30 | def __init__(self, master): 31 | self.master = master 32 | self.config = master.config 33 | try: 34 | self.configConfig = master.config["config"] 35 | except KeyError: 36 | self.configConfig = {} 37 | try: 38 | self.configLogging = master.config["logging"]["Console"] 39 | except KeyError: 40 | self.configLogging = {} 41 | self.status = self.configLogging.get("enabled", True) 42 | 43 | # Unload if this module is disabled or misconfigured 44 | if not self.status: 45 | self.master.releaseModule("lib.TWCManager.Logging", "ConsoleLogging") 46 | return None 47 | 48 | # Initialize the mute config tree if it is not already 49 | if not self.configLogging.get("mute", None): 50 | self.configLogging["mute"] = {} 51 | 52 | # Initialize Logger 53 | handler = logging.StreamHandler(sys.stdout) 54 | if self.configLogging.get("simple", False): 55 | handler.setFormatter( 56 | logging.Formatter("%(name)-10.10s %(levelno)02d %(message)s") 57 | ) 58 | else: 59 | color_formatter = ColorFormatter( 60 | colored("%(asctime)s", "yellow") 61 | + " " 62 | + colored("%(name)-10.10s", "green") 63 | + " " 64 | + colored("%(levelno)d", "cyan") 65 | + " %(message)s", 66 | "%H:%M:%S", 67 | ) 68 | handler.setFormatter(color_formatter) 69 | # handler.setLevel(logging.INFO) 70 | logging.getLogger("").addHandler(handler) 71 | 72 | def getCapabilities(self, capability): 73 | # Allows query of module capabilities when deciding which Logging module to use 74 | return self.capabilities.get(capability, False) 75 | -------------------------------------------------------------------------------- /lib/TWCManager/Logging/FileLogging.py: -------------------------------------------------------------------------------- 1 | # ConsoleLogging module. Provides output to console for logging. 2 | import logging 3 | 4 | from sys import modules 5 | import logging 6 | from logging.handlers import TimedRotatingFileHandler 7 | import re 8 | 9 | 10 | logger = logging.getLogger(__name__.rsplit(".")[-1]) 11 | 12 | 13 | class FileLogging: 14 | capabilities = {"queryGreenEnergy": False} 15 | config = None 16 | configConfig = None 17 | configLogging = None 18 | status = True 19 | logger = None 20 | mute = {} 21 | muteDebugLogLevelGreaterThan = 1 22 | 23 | def __init__(self, master): 24 | self.master = master 25 | self.config = master.config 26 | try: 27 | self.configConfig = master.config["config"] 28 | except KeyError: 29 | self.configConfig = {} 30 | try: 31 | self.configLogging = master.config["logging"]["FileLogger"] 32 | except KeyError: 33 | self.configLogging = {} 34 | self.status = self.configLogging.get("enabled", False) 35 | 36 | # Unload if this module is disabled or misconfigured 37 | if not self.status: 38 | self.master.releaseModule("lib.TWCManager.Logging", "FileLogging") 39 | return None 40 | 41 | # Initialize the mute config tree if it is not already 42 | self.mute = self.configLogging.get("mute", {}) 43 | self.muteDebugLogLevelGreaterThan = self.mute.get("DebugLogLevelGreaterThan", 1) 44 | 45 | # Initialize Logger 46 | handler = None 47 | try: 48 | handler = TimedRotatingFileHandler( 49 | self.configLogging.get("path", "/etc/twcmanager/log") + "/logfile", 50 | when="H", 51 | interval=1, 52 | backupCount=24, 53 | ) 54 | except PermissionError: 55 | logger.error("Permission Denied error opening logfile for writing") 56 | if handler: 57 | handler.setFormatter( 58 | logging.Formatter( 59 | "%(asctime)s - %(name)-10.10s %(levelno)02d %(message)s" 60 | ) 61 | ) 62 | logging.getLogger("").addHandler(handler) 63 | 64 | def getCapabilities(self, capability): 65 | # Allows query of module capabilities when deciding which Logging module to use 66 | return self.capabilities.get(capability, False) 67 | -------------------------------------------------------------------------------- /lib/TWCManager/Logging/SentryLogging.py: -------------------------------------------------------------------------------- 1 | # SentryLogging module. Provides output to console for logging. 2 | import logging 3 | 4 | import sentry_sdk 5 | from sentry_sdk.integrations.logging import LoggingIntegration 6 | 7 | logger = logging.getLogger(__name__.rsplit(".")[-1]) 8 | 9 | 10 | class SentryLogging: 11 | capabilities = {"queryGreenEnergy": False} 12 | config = None 13 | configConfig = None 14 | configLogging = None 15 | status = True 16 | logger = None 17 | mute = {} 18 | muteDebugLogLevelGreaterThan = 1 19 | 20 | def __init__(self, master): 21 | # raise ImportError 22 | self.master = master 23 | self.config = master.config 24 | try: 25 | self.configConfig = master.config["config"] 26 | except KeyError: 27 | self.configConfig = {} 28 | try: 29 | self.configLogging = master.config["logging"]["Sentry"] 30 | except KeyError: 31 | self.configLogging = {} 32 | self.status = self.configLogging.get("enabled", False) 33 | self.dsn = self.configLogging.get("DSN", False) 34 | 35 | # Unload if this module is disabled or misconfigured 36 | if not self.status or not self.dsn: 37 | self.master.releaseModule("lib.TWCManager.Logging", "SentryLogging") 38 | return None 39 | 40 | # Initialize the mute config tree if it is not already 41 | self.mute = self.configLogging.get("mute", {}) 42 | self.muteDebugLogLevelGreaterThan = self.mute.get("DebugLogLevelGreaterThan", 1) 43 | 44 | # Initialize Logger 45 | sentry_logging = LoggingIntegration( 46 | level=logging.INFO, # Capture info and above as breadcrumbs 47 | event_level=logging.ERROR, # Send errors as events 48 | ) 49 | sentry_sdk.init(self.dsn, integrations=[sentry_logging], traces_sample_rate=1.0) 50 | 51 | def getCapabilities(self, capability): 52 | # Allows query of module capabilities when deciding which Logging module to use 53 | return self.capabilities.get(capability, False) 54 | -------------------------------------------------------------------------------- /lib/TWCManager/Status/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /lib/TWCManager/Vehicle/FleetTelemetryMQTT.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import psycopg2 3 | import paho.mqtt.client as mqtt 4 | import threading 5 | import time 6 | import json 7 | 8 | from TWCManager.Vehicle.Telemetry import TelmetryBase 9 | 10 | logger = logging.getLogger("\U0001f697 FleetTLM") 11 | 12 | 13 | class FleetTelemetryMQTT(TelmetryBase): 14 | configName = "teslaFleetTelemetryMQTT" 15 | vehicleNameTopic = "VehicleName" 16 | events = { 17 | "BatteryLevel": ["batteryLevel", lambda a: int(float(a))], 18 | "ChargeLimitSoc": ["chargeLimit", lambda a: int(float(a))], 19 | "VehicleName": ["name", lambda a: a], 20 | "latitude": ["syncLat", lambda a: float(a)], 21 | "longitude": ["syncLon", lambda a: float(a)], 22 | "syncState": ["syncState", lambda a: a], 23 | "TimeToFullCharge": ["timeToFullCharge", lambda a: float(a)], 24 | "ChargeCurrentRequest": ["availableCurrent", lambda a: int(a)], 25 | "ChargeAmps": ["actualCurrent", lambda a: float(a)], 26 | "ChargerPhases": ["phases", lambda a: int(a) if a else 0], 27 | "ChargerVoltage": ["voltage", lambda a: float(a)], 28 | "DetailedChargeState": ["chargingState", lambda a: a[19:]], 29 | } 30 | 31 | def mqttConnect(self, client, userdata, flags, rc, properties=None): 32 | logger.log(logging.INFO5, "MQTT Connected.") 33 | logger.log(logging.INFO5, "Subscribe to " + self.mqtt_prefix + "/#") 34 | res = self.client.subscribe(self.mqtt_prefix + "/#", qos=1) 35 | logger.log(logging.INFO5, "Res: " + str(res)) 36 | 37 | def mqttMessage(self, client, userdata, message): 38 | topic = str(message.topic).split("/") 39 | try: 40 | payload = json.loads(message.payload.decode("utf-8")) 41 | except json.decoder.JSONDecodeError: 42 | logger.warning(f"Can't decode payload {payload} in topic {message.topic}") 43 | return 44 | 45 | # Topic format is telemetry/VEHICLE-VIN/v/ChargerVoltage 46 | if topic[0] != self.mqtt_prefix: 47 | return 48 | 49 | syncState = ( 50 | self.vehicles[topic[1]].syncState if self.vehicles.get(topic[1]) else "" 51 | ) 52 | if len(topic) > 3 and topic[2] == "v": 53 | if topic[3] == "Gear": 54 | if payload in ( 55 | "R", 56 | "N", 57 | "D", 58 | ): 59 | self.applyDataToVehicle(topic[1], "syncState", "driving") 60 | elif syncState == "driving": 61 | self.applyDataToVehicle(topic[1], "syncState", "online") 62 | elif topic[3] == "DetailedChargeState": 63 | self.applyDataToVehicle(topic[1], topic[3], payload) 64 | if payload == "DetailedChargeStateCharging": 65 | self.applyDataToVehicle(topic[1], "syncState", "charging") 66 | elif syncState == "charging": 67 | self.applyDataToVehicle(topic[1], "syncState", "online") 68 | elif topic[3] == "Location" and isinstance(payload, dict): 69 | if payload.get("latitude"): 70 | self.applyDataToVehicle(topic[1], "latitude", payload["latitude"]) 71 | if payload.get("longitude"): 72 | self.applyDataToVehicle(topic[1], "longitude", payload["longitude"]) 73 | else: 74 | self.applyDataToVehicle(topic[1], topic[3], payload) 75 | elif len(topic) > 2 and topic[2] == "connectivity": 76 | status = payload.get("Status") 77 | if status == "CONNECTED" and syncState not in ( 78 | "driving", 79 | "charging", 80 | ): 81 | self.applyDataToVehicle(topic[1], "syncState", "online") 82 | elif status == "DISCONNECTED": 83 | self.applyDataToVehicle(topic[1], "syncState", "offline") 84 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography<3.4 2 | build 3 | growattServer>=1.0.0 4 | jinja2>=2.11.2 5 | ocpp 6 | paho_mqtt>=1.5.0 7 | psycopg2 8 | pymodbus<3.0.0; python_version < '3.8' 9 | pymodbus>=3.0.0; python_version >= '3.8' 10 | pyModbusTCP>=0.1.8 11 | pymysql 12 | pyserial>=3.4 13 | PyJWT 14 | PyYAML 15 | requests>=2.23.0 16 | sentry_sdk>=0.11.2 17 | solaredge_modbus>=0.7.0; python_version >= '3.7' 18 | sysv_ipc 19 | termcolor>=1.1.0 20 | websockets<=9.1; python_version == '3.6' 21 | websockets>=9.1; python_version >= '3.7' 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from setuptools import setup, find_namespace_packages 4 | 5 | setup( 6 | name="TWCManager", 7 | version="1.3.2", 8 | package_dir={"": "lib"}, 9 | packages=find_namespace_packages(where="lib"), 10 | python_requires=">= 3.6", 11 | include_package_data=True, 12 | long_description="Controls the charge rate of certain versions of Tesla Wall Connector (TWC) via the built-in Load Sharing protocol.", 13 | # Dependencies 14 | install_requires=[ 15 | "cryptography<3.4", 16 | "growattServer>=1.0.0", 17 | "jinja2>=2.11.2", 18 | "ocpp", 19 | "paho_mqtt>=1.5.0", 20 | "psycopg2", 21 | "pyjwt", 22 | "pyModbusTCP>=0.1.8", 23 | "pymysql", 24 | "pyserial>=3.4", 25 | "pyyaml", 26 | "requests>=2.23.0", 27 | "sentry_sdk>=0.11.2", 28 | "solaredge_modbus>=0.7.0", 29 | "sysv_ipc", 30 | "termcolor>=1.1.0", 31 | "websockets<=9.1; python_version == '3.6'", 32 | "websockets>=9.1; python_version >= '3.7'", 33 | ], 34 | # Package Metadata 35 | author="Nathan Gardiner", 36 | author_email="ngardiner@gmail.com", 37 | description="Package to manage Tesla Wall Connector installations", 38 | keywords="tesla wall connector charger", 39 | url="https://github.com/ngardiner/twcmanager", 40 | ) 41 | -------------------------------------------------------------------------------- /tests/API/test_apilistener.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LISTEN=`ss -ntulw | grep LISTEN | grep -c 8088` 4 | 5 | if [ $LISTEN -lt 1 ]; then 6 | echo Error: Port is not listening 7 | exit 255 8 | else 9 | echo Test passed: API Port is listening 10 | exit 0 11 | fi 12 | -------------------------------------------------------------------------------- /tests/API/test_getActivePolicyAction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import requests 5 | 6 | # Configuration 7 | skipFailure = 1 8 | 9 | # Disable environment import to avoid proxying requests 10 | session = requests.Session() 11 | session.trust_env = False 12 | 13 | getAttempts = 0 14 | response = None 15 | 16 | try: 17 | response = session.get("http://127.0.0.1:8088/api/getActivePolicyAction", timeout=30) 18 | except requests.Timeout: 19 | print("Error: Connection Timed Out") 20 | exit(255) 21 | except requests.ConnectionError: 22 | print("Error: Connection Error") 23 | exit(255) 24 | 25 | jsonResp = None 26 | 27 | if response.status_code == 200: 28 | while (not jsonResp and getAttempts < 3): 29 | getAttempts += 1 30 | try: 31 | jsonResp = response.json() 32 | except json.decoder.JSONDecodeError as e: 33 | print("Error: Unable to parse JSON output from getActivePolicyAction()") 34 | 35 | # Log the incomplete JSON that we did get - I would like to know 36 | # why this would happen 37 | f = open("/tmp/twcmanager-tests/getActivePolicyAction-json-"+str(getAttempts)+".txt", "w") 38 | f.write("Exception: " + str(e)) 39 | f.write("API Response: " + str(response.text)) 40 | 41 | if (getAttempts == 2): 42 | # Too many failures 43 | # Fail tests 44 | exit(255) 45 | else: 46 | print("Error: Response code " + str(response.status_code)) 47 | exit(255) 48 | 49 | success = 1 50 | if jsonResp: 51 | # Content tests go here 52 | if jsonResp == 1 or jsonResp == 2 or jsonResp == 3: 53 | success = 1 54 | else: 55 | print(f"got unknown policy action: {jsonResp!r}") 56 | else: 57 | print("No JSON response from API for getActivePolicyAction()") 58 | exit(255) 59 | 60 | if success: 61 | print("All tests successful") 62 | exit(0) 63 | else: 64 | print("At least one test failed. Please review logs") 65 | if skipFailure: 66 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 67 | exit(0) 68 | else: 69 | exit(255) 70 | -------------------------------------------------------------------------------- /tests/API/test_getLastTWCResponse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import requests 5 | 6 | # Configuration 7 | skipFailure = 0 8 | 9 | # Disable environment import to avoid proxying requests 10 | session = requests.Session() 11 | session.trust_env = False 12 | 13 | success = 1 14 | response = None 15 | 16 | try: 17 | response = session.get("http://127.0.0.1:8088/api/getLastTWCResponse", timeout=30) 18 | except requests.Timeout: 19 | print("Error: Connection Timed Out") 20 | exit(255) 21 | except requests.ConnectionError: 22 | print("Error: Connection Error") 23 | exit(255) 24 | 25 | if response.status_code == 200: 26 | success = 1 27 | else: 28 | print("Error: Response code " + str(response.status_code)) 29 | exit(255) 30 | 31 | if success: 32 | print("All tests successful") 33 | exit(0) 34 | else: 35 | print("At least one test failed. Please review logs") 36 | if skipFailure: 37 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 38 | exit(0) 39 | else: 40 | exit(255) 41 | -------------------------------------------------------------------------------- /tests/API/test_getPolicy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import requests 5 | 6 | # Configuration 7 | skipFailure = 1 8 | 9 | # Disable environment import to avoid proxying requests 10 | session = requests.Session() 11 | session.trust_env = False 12 | 13 | getAttempts = 0 14 | response = None 15 | 16 | try: 17 | response = session.get("http://127.0.0.1:8088/api/getPolicy", timeout=30) 18 | except requests.Timeout: 19 | print("Error: Connection Timed Out") 20 | exit(255) 21 | except requests.ConnectionError: 22 | print("Error: Connection Error") 23 | exit(255) 24 | 25 | jsonResp = None 26 | 27 | if response.status_code == 200: 28 | while (not jsonResp and getAttempts < 3): 29 | getAttempts += 1 30 | try: 31 | jsonResp = response.json() 32 | except json.decoder.JSONDecodeError as e: 33 | print("Error: Unable to parse JSON output from getPolicy()") 34 | 35 | # Log the incomplete JSON that we did get - I would like to know 36 | # why this would happen 37 | f = open("/tmp/twcmanager-tests/getPolicy-json-"+str(getAttempts)+".txt", "w") 38 | f.write("Exception: " + str(e)) 39 | f.write("API Response: " + str(response.text)) 40 | 41 | if (getAttempts == 2): 42 | # Too many failures 43 | # Fail tests 44 | exit(255) 45 | else: 46 | print("Error: Response code " + str(response.status_code)) 47 | exit(255) 48 | 49 | success = 1 50 | if jsonResp: 51 | # Content tests go here 52 | success = 1 53 | else: 54 | print("No JSON response from API for getPolicy()") 55 | exit(255) 56 | 57 | if success: 58 | print("All tests successful") 59 | exit(0) 60 | else: 61 | print("At least one test failed. Please review logs") 62 | if skipFailure: 63 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 64 | exit(0) 65 | else: 66 | exit(255) 67 | -------------------------------------------------------------------------------- /tests/API/test_getSlaveTWCs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | 5 | # Disable environment import to avoid proxying requests 6 | session = requests.Session() 7 | session.trust_env = False 8 | 9 | #b'{"4142": {"lastAmpsOffered": 0, "lastHeartbeat": 0.68, "state": 0, "TWCID": "4142", "voltsPhaseC": 0, "reportedAmpsActual": 0.0, "lastVIN": "", "maxAmps": 80.0, "lifetimekWh": 0, "version": 2, "currentVIN": "", "voltsPhaseA": 0, "voltsPhaseB": 0}, "total": {"lifetimekWh": 0, "reportedAmpsActual": 0.0, "TWCID": "total", "maxAmps": 80.0, "lastAmpsOffered": 0}}' 10 | # Todo Tests: 11 | # Send specific lifetime kWh message and compare values 12 | 13 | response = None 14 | 15 | try: 16 | response = session.get("http://127.0.0.1:8088/api/getSlaveTWCs", timeout=5) 17 | except requests.Timeout: 18 | print("Error: Connection Timed Out") 19 | exit(255) 20 | except requests.ConnectionError: 21 | print("Error: Connection Error") 22 | exit(255) 23 | 24 | json = None 25 | 26 | if response.status_code == 200: 27 | json = response.json() 28 | else: 29 | print("Error: Response code " + str(response.status_code)) 30 | exit(255) 31 | 32 | success = 1 33 | 34 | if json: 35 | if not json["4142"]: 36 | success = 0 37 | print("Error: Could not find TWC 4142 in getSlaveTWCs() output") 38 | if json["4142"]["lastHeartbeat"] > 5: 39 | success = 0 40 | print("Error: TWC 4142 has not responded with heartbeat message in 5 seconds") 41 | if json["4142"]["maxAmps"] != 80: 42 | success = 0 43 | print("Detected Maximum Amperage for TWC 4142 is incorrect") 44 | else: 45 | print("No JSON response from API for getConfig()") 46 | exit(255) 47 | 48 | if success: 49 | print("All tests successful") 50 | exit(0) 51 | else: 52 | print("At least one test failed. Please review logs") 53 | exit(255) 54 | -------------------------------------------------------------------------------- /tests/API/test_getStatus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | 5 | # Disable environment import to avoid proxying requests 6 | session = requests.Session() 7 | session.trust_env = False 8 | 9 | #b'{"scheduledChargingStartHour": -1, "consumptionAmps": "0.00", "isGreenPolicy": "No", "scheduledChargingFlexStart": -1, "chargerLoadWatts": "0.00", "currentPolicy": "Non Scheduled Charging", "generationWatts": "0.00", "carsCharging": 0, "scheduledChargingEndHour": -1, "maxAmpsToDivideAmongSlaves": "0.00", "consumptionWatts": "0.00", "ScheduledCharging": {"tuesday": true, "monday": true, "flexStartingMinute": -1, "flexSaturday": true, "flexFriday": true, "saturday": true, "wednesday": true, "flexTuesday": true, "thursday": true, "flexWednesday": true, "flexSunday": true, "flexBatterySize": 100, "amps": 0, "flexEndingMinute": -1, "sunday": true, "flexThursday": true, "flexStartEnabled": 0, "enabled": false, "endingMinute": -1, "flexMonday": true, "friday": true, "startingMinute": -1}, "generationAmps": "0.00"}' 10 | 11 | response = None 12 | 13 | try: 14 | response = session.get("http://127.0.0.1:8088/api/getStatus", timeout=5) 15 | except requests.Timeout: 16 | print("Error: Connection Timed Out") 17 | exit(255) 18 | except requests.ConnectionError: 19 | print("Error: Connection Error") 20 | exit(255) 21 | 22 | json = None 23 | 24 | if response.status_code == 200: 25 | json = response.json() 26 | else: 27 | print("Error: Response code " + str(response.status_code)) 28 | exit(255) 29 | -------------------------------------------------------------------------------- /tests/API/test_sendStartCommand.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import requests 5 | 6 | # Configuration 7 | skipFailure = 0 8 | 9 | # Disable environment import to avoid proxying requests 10 | session = requests.Session() 11 | session.trust_env = False 12 | 13 | success = 1 14 | response = None 15 | 16 | try: 17 | response = session.post("http://127.0.0.1:8088/api/sendStartCommand", timeout=30) 18 | except requests.Timeout: 19 | print("Error: Connection Timed Out") 20 | exit(255) 21 | except requests.ConnectionError: 22 | print("Error: Connection Error") 23 | exit(255) 24 | 25 | if response.status_code == 204: 26 | success = 1 27 | else: 28 | print("Error: Response code " + str(response.status_code)) 29 | success = 0 30 | 31 | if success: 32 | print("All tests successful") 33 | exit(0) 34 | else: 35 | print("At least one test failed. Please review logs") 36 | if skipFailure: 37 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 38 | exit(0) 39 | else: 40 | exit(255) 41 | -------------------------------------------------------------------------------- /tests/API/test_sendStopCommand.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import requests 5 | 6 | # Configuration 7 | skipFailure = 0 8 | 9 | # Disable environment import to avoid proxying requests 10 | session = requests.Session() 11 | session.trust_env = False 12 | 13 | success = 1 14 | response = None 15 | 16 | try: 17 | response = session.post("http://127.0.0.1:8088/api/sendStopCommand", timeout=30) 18 | except requests.Timeout: 19 | print("Error: Connection Timed Out") 20 | success = 0 21 | except requests.ConnectionError: 22 | print("Error: Connection Error") 23 | success = 0 24 | 25 | if response.status_code == 204: 26 | success = 1 27 | else: 28 | print("Error: Response code " + str(response.status_code)) 29 | success = 0 30 | 31 | if success: 32 | print("All tests successful") 33 | exit(0) 34 | else: 35 | print("At least one test failed. Please review logs") 36 | if skipFailure: 37 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 38 | exit(0) 39 | else: 40 | exit(255) 41 | -------------------------------------------------------------------------------- /tests/API/test_setLatLon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | 5 | # Configuration 6 | skipFailure = 0 7 | 8 | # Disable environment import to avoid proxying requests 9 | session = requests.Session() 10 | session.trust_env = False 11 | 12 | response = None 13 | 14 | values = { 15 | "elapsed": {}, 16 | "expected": {}, 17 | "response": {}, 18 | "status": {}, 19 | "tests": {}, 20 | "text": {} 21 | } 22 | 23 | # Test 1 - Set Home Latitude setting 24 | data = { 25 | "setting": "x", 26 | "value": "x" 27 | } 28 | 29 | values["tests"]["setHomeLat"] = {} 30 | try: 31 | response = session.post("http://127.0.0.1:8088/api/setSetting", json=data, timeout=5) 32 | values["elapsed"]["setHomeLat"] = response.elapsed 33 | values["response"]["setHomeLat"] = response.status_code 34 | except requests.Timeout: 35 | print("Error: Connection Timed Out") 36 | values["tests"]["setHomeLat"]["fail"] = 1 37 | except requests.ConnectionError: 38 | print("Error: Connection Error") 39 | values["tests"]["setHomeLat"]["fail"] = 1 40 | 41 | # Test 2 - Set Home Longitude setting 42 | data = { 43 | "setting": "x", 44 | "value": "x" 45 | } 46 | 47 | values["tests"]["setHomeLon"] = {} 48 | try: 49 | response = session.post("http://127.0.0.1:8088/api/setSetting", json=data, timeout=5) 50 | values["elapsed"]["setHomeLon"] = response.elapsed 51 | values["response"]["setHomeLon"] = response.status_code 52 | except requests.Timeout: 53 | print("Error: Connection Timed Out") 54 | values["tests"]["setHomeLon"]["fail"] = 1 55 | except requests.ConnectionError: 56 | print("Error: Connection Error") 57 | values["tests"]["setHomeLon"]["fail"] = 1 58 | 59 | for test in values["tests"].keys(): 60 | if values["tests"][test].get("fail", 0): 61 | print("At least one test failed. Please review logs") 62 | if skipFailure: 63 | print("Due to skipFailure being set, we will not fail the test suite pipeline on this test.") 64 | exit(0) 65 | else: 66 | exit(255) 67 | 68 | print("All tests were successful") 69 | exit(0) 70 | 71 | -------------------------------------------------------------------------------- /tests/EMS/ems_emulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from http.server import HTTPServer, BaseHTTPRequestHandler 4 | 5 | class MyRequestHandler(BaseHTTPRequestHandler): 6 | 7 | def do_GET(self): 8 | 9 | # Extract values from the query string 10 | path, _, query_string = self.path.partition('?') 11 | query = parse_qs(query_string) 12 | 13 | self.send_response(200) 14 | self.end_headers() 15 | 16 | if path == "/api/all/power/now": 17 | self.wfile.write(emulate_SmartPi()) 18 | 19 | def emulate_SmartPi(): 20 | return('{"serial":"smartpi123412345","name":"House","lat":111.1111,"lng":2.2222,"time":"2021-03-22 19:36:38","softwareversion":"","ipaddress":"192.168.1.3","datasets":[{"time":"2021-03-22 19:36:38","phases":[{"phase":1,"name":"phase 1","values":[{"type":"power","unity":"W","info":"","data":168.92613}]},{"phase":2,"name":"phase 2","values":[{"type":"power","unity":"W","info":"","data":212.23642}]},{"phase":3,"name":"phase 3","values":[{"type":"power","unity":"W","info":"","data":91.89515}]},{"phase":4,"name":"phase 4","values":[{"type":"power","unity":"W","info":"","data":0}]}]}]}'); 21 | 22 | httpd = HTTPServer(('localhost', 1080), MyRequestHandler) 23 | httpd.serve_forever() 24 | -------------------------------------------------------------------------------- /tests/EMS/test_MQTT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import paho.mqtt.client as mqtt 4 | import time 5 | 6 | def mqttConnected(client, userdata, flags, rc, properties=None): 7 | global test_state 8 | test_state = 1 9 | 10 | connection_time = 0 11 | test_duration = 0 12 | test_duration_max = 120 13 | test_state = 0 14 | 15 | if hasattr(mqtt, 'CallbackAPIVersion'): 16 | client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, "MQTT.EMS.Test", protocol=mqtt.MQTTv5) 17 | else: 18 | client = mqtt.Client("MQTT.EMS.Test") 19 | client.username_pw_set("twcmanager", "twcmanager") 20 | client.on_connect = mqttConnected 21 | 22 | client.connect_async( 23 | "127.0.0.1", port=1883, keepalive=30 24 | ) 25 | 26 | # Run this test for a maximum of test_duration_max or until the test has completed, whichever comes first 27 | while (test_duration < test_duration_max): 28 | if test_state == 0: 29 | # Waiting on connection to the MQTT Broker 30 | connection_time = test_duration 31 | elif test_state == 1: 32 | # Connection Established. Update MQTT topic. 33 | client.publish("/test", "test", qos=0) 34 | test_state = 2 35 | test_duration += 1 36 | time.sleep(0.1) 37 | -------------------------------------------------------------------------------- /tests/EMS/test_SmartPi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | all: test_direct 2 | test_direct: general_prereqs mqtt_setup mysql_setup run_twcmanager wait preflight api ems 3 | test_service: general_prereqs mqtt_setup mysql_setup run_service wait preflight api ems 4 | test_service_nofail: general_prereqs mqtt_setup mysql_setup run_service_nofail wait 5 | 6 | bold:=$(shell tput -T ansi bold) 7 | red:=$(shell tput -T ansi setaf 1) 8 | reset:=$(shell tput -T ansi sgr0) 9 | yellow:=$(shell tput -T ansi setaf 3) 10 | 11 | api := 10 12 | ems := 2 13 | pre := 7 14 | 15 | general_prereqs: 16 | @echo "${bold}${red}(P1/${pre})${reset} ${yellow}Installing Prerequisites for Test Suite" 17 | @sudo apt-get install -y debconf-utils iproute2 > /dev/null 2>&1 18 | @mkdir -p /tmp/twcmanager-tests 19 | @chmod 777 /tmp/twcmanager-tests 20 | 21 | mqtt_setup: 22 | @echo "${bold}${red}(P2/${pre})${reset} ${yellow}Installing MQTT Prerequisites" 23 | @sudo apt-get install -y mosquitto > /dev/null 2>&1 24 | 25 | mysql_setup: 26 | @echo "${bold}${red}(P3/${pre})${reset} ${yellow}Installing MySQL Server" 27 | @sudo apt-get install -y mysql-server sqlite3 > /dev/null 2>&1 28 | @echo "${bold}${red}(P4/${pre})${reset} ${yellow}Setup MySQL Database" 29 | @sudo /home/docker/.pyenv/shims/python3 scripts/mysql_setup.py 30 | @echo "${bold}${red}(P5/${pre})${reset} ${yellow}Environment Preparation" 31 | @sudo /home/docker/.pyenv/shims/python3 pre-flight/env_preparation.py 32 | @echo "${bold}${red}(P6/${pre})${reset} ${yellow}Set up SQLite Database" 33 | @sudo -u twcmanager /home/docker/.pyenv/shims/python3 scripts/sqlite_setup.py 34 | 35 | run_service: 36 | @cd .. && sudo cp contrib/.twcmanager.service.testing /etc/systemd/system/twcmanager.service 37 | @sudo apt-get install -y systemctl > /dev/null 38 | @echo "${bold}${red}(M1/2)${reset} ${yellow}Running TWCManager" 39 | @sudo /usr/bin/systemctl enable twcmanager.service --now 40 | @sleep 5 41 | @systemctl status twcmanager.service --full 42 | 43 | run_service_nofail: 44 | @cd .. && sudo cp contrib/.twcmanager.service.testing /etc/systemd/system/twcmanager.service 45 | @sudo apt-get install -y systemctl > /dev/null 46 | @echo "${bold}${red}(M1/2)${reset} ${yellow}Running TWCManager" 47 | @sudo /usr/bin/systemctl enable twcmanager.service --now 48 | @sleep 5 49 | @systemctl status twcmanager.service --full || exit 0 50 | ls -l /var/log/ 51 | 52 | run_twcmanager: 53 | @echo "${bold}${red}(M1/2)${reset} ${yellow}Running TWCManager" 54 | @cd .. && sudo -u twcmanager /usr/bin/env PYTHONIOENCODING=UTF-8 /home/docker/.pyenv/shims/python3 -m TWCManager & 55 | 56 | upload: 57 | @echo Uploading debug files 58 | @find /tmp/twcmanager-tests -type f | xargs -L 1 scripts/upload_file.sh 59 | @rm -r /tmp/twcmanager-tests 60 | 61 | api: 62 | @echo "${bold}${red}(A1/${api})${reset} ${yellow}Test API Listener" 63 | @cd API && ./test_apilistener.sh 64 | @echo "${bold}${red}(A2/${api})${reset} ${yellow}Test API getConfig" 65 | @cd API && ./test_getConfig.py 66 | @echo "${bold}${red}(A3/${api})${reset} ${yellow}Test API getLastTWCResponse" 67 | @cd API && ./test_getLastTWCResponse.py 68 | @echo "${bold}${red}(A4/${api})${reset} ${yellow}Test API setLatLon" 69 | @cd API && ./test_setLatLon.py 70 | @echo "${bold}${red}(A5/${api})${reset} ${yellow}Test API getPolicy" 71 | @cd API && ./test_getPolicy.py 72 | @echo "${bold}${red}(A6/${api})${reset} ${yellow}Test API getSlaveTWCs" 73 | @cd API && ./test_getSlaveTWCs.py 74 | @echo "${bold}${red}(A7/${api})${reset} ${yellow}Test API getStatus" 75 | @cd API && ./test_getStatus.py 76 | @echo "${bold}${red}(A8/${api})${reset} ${yellow}Test API chargeNow functionality" 77 | @cd API && ./test_chargeNow.py 78 | @echo "${bold}${red}(A9/${api})${reset} ${yellow}Test API sendStartCommand" 79 | @cd API && ./test_sendStartCommand.py 80 | @echo "${bold}${red}(A10/${api})${reset} ${yellow}Test API sendStopCommand" 81 | @cd API && ./test_sendStopCommand.py 82 | @echo "${bold}${red}(A11/${api})${reset} ${yellow}Test API consumption offsets" 83 | @cd API && ./test_consumptionOffsets.py 84 | @echo "${bold}${red}(A12/${api})${reset} ${yellow}Test API getActivePolicyAction" 85 | @cd API && ./test_getActivePolicyAction.py 86 | 87 | ems: 88 | @echo "${bold}${red}(E1/${ems})${reset} ${yellow}Starting EMS Test Web Server" 89 | @cd EMS && ./ems_emulator.py & 90 | @echo "${bold}${red}(E2/${ems})${reset} ${yellow}Testing SmartPi EMS Module" 91 | cd EMS && ./test_SmartPi.py 92 | @echo "${bold}${red}(E3/${ems})${reset} ${yellow}Testing MQTT EMS Module" 93 | cd EMS && ./test_MQTT.py 94 | 95 | preflight: 96 | @echo "${bold}${red}(P7/${pre})${reset} ${yellow}Testing file existence and permissions" 97 | @cd pre-flight && ./check_environment.py 98 | ls -l /etc/twcmanager 99 | 100 | wait: 101 | @echo "${bold}${red}(M2/2)${reset} ${yellow}Wait for TWCManager to start" 102 | @sleep 120 103 | -------------------------------------------------------------------------------- /tests/pre-flight/check_environment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import stat 5 | from grp import getgrgid 6 | from pwd import getpwuid 7 | 8 | # check_environment.py 9 | # 10 | # This script will check that the install completed successfully. 11 | # We'll check that the correct packages were installed, that /etc/twcmanager was created 12 | # and that the permissions are correct 13 | 14 | success = 1 15 | 16 | def check_file(filename, exp_user, exp_group, exp_mode): 17 | global success 18 | 19 | if os.path.exists(filename): 20 | # Good. Check permissions 21 | if not getpwuid(os.stat(filename).st_uid).pw_name == exp_user: 22 | # Owner is wrong 23 | print("Wrong ownership for %s" % filename) 24 | success = 0 25 | 26 | if not getgrgid(os.stat(filename).st_gid).gr_name == exp_group: 27 | # Group is wrong 28 | print("Wrong group ownership for %s" % filename) 29 | success = 0 30 | 31 | # Set expected mode for directory 32 | expmode = int(exp_mode, 8) 33 | if not stat.S_IMODE(os.stat(filename).st_mode) == expmode: 34 | # Directory permissions are wrong 35 | print("Wrong permissions for %s" % filename) 36 | success = 0 37 | 38 | else: 39 | # Uh oh, error 40 | print("Error: %s doesn't exist" % filename) 41 | success = 0 42 | 43 | check_file("/etc/twcmanager", "twcmanager", "twcmanager", "755") 44 | check_file("/etc/twcmanager/config.json", "twcmanager", "twcmanager", "755") 45 | 46 | if success: 47 | print("All tests passed") 48 | exit(0) 49 | else: 50 | print("At least one test failed") 51 | exit(255) 52 | -------------------------------------------------------------------------------- /tests/pre-flight/env_preparation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from grp import getgrnam 4 | from pwd import getpwnam 5 | import os 6 | import subprocess 7 | 8 | #print("Create files") 9 | gid = getgrnam('twcmanager').gr_gid 10 | uid = getpwnam('twcmanager').pw_uid 11 | os.makedirs("/etc/twcmanager/csv", exist_ok=True) 12 | os.chown("/etc/twcmanager/csv", uid, gid) 13 | 14 | os.makedirs("/etc/twcmanager/log", exist_ok=True) 15 | os.chown("/etc/twcmanager/log", uid, gid) 16 | 17 | sqlite = "/etc/twcmanager/twcmanager.sqlite" 18 | fhandle = open(sqlite, "a") 19 | try: 20 | os.utime(sqlite, None) 21 | finally: 22 | fhandle.close() 23 | os.chown(sqlite, uid, gid) 24 | os.chmod(sqlite, 0o664) 25 | 26 | #print("Stopping mosquitto...") 27 | devnull = open(os.devnull, 'w') 28 | subprocess.call(["service", "mosquitto", "stop"], stdout=devnull, stderr=devnull) 29 | 30 | mospwd = open("/etc/mosquitto/passwd", "w+") 31 | mospwd.write("twcmanager:twcmanager") 32 | mospwd.close() 33 | 34 | #print("Converting mosquitto password file") 35 | subprocess.call(["mosquitto_passwd", "-U", "/etc/mosquitto/passwd"]) 36 | 37 | #print("Starting mosquitto...") 38 | subprocess.call(["service", "mosquitto", "start"], stdout=devnull, stderr=devnull) 39 | -------------------------------------------------------------------------------- /tests/scripts/mysql_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | 6 | def execute_query(query, database): 7 | 8 | queryb = query.encode('utf-8') 9 | process = subprocess.Popen(['mysql', '-ss', database], 10 | stdout=subprocess.PIPE, 11 | stderr=subprocess.STDOUT, 12 | stdin=subprocess.PIPE) 13 | 14 | stdout, _ = process.communicate(input=queryb) 15 | 16 | if process.returncode != 0: 17 | print("Query failed: %s" % query) 18 | print("Output was: %s" % stdout) 19 | 20 | devnull = open(os.devnull, 'w') 21 | #print("Starting mysql server...") 22 | subprocess.call(["service", "mysql", "start"], stdout=devnull, stderr=devnull) 23 | 24 | execute_query("CREATE DATABASE twcmanager;", "mysql") 25 | execute_query("CREATE USER 'twcmanager'@'localhost' IDENTIFIED BY 'twcmanager';", "mysql") 26 | execute_query("GRANT ALL PRIVILEGES ON twcmanager.* TO 'twcmanager'@'localhost';", "mysql") 27 | execute_query("""CREATE TABLE charge_sessions ( 28 | chargeid int, 29 | startTime datetime, 30 | startkWh int, 31 | slaveTWC varchar(4), 32 | endTime datetime, 33 | endkWh int, 34 | vehicleVIN varchar(17), 35 | primary key(startTime, slaveTWC) 36 | ); 37 | """, "twcmanager") 38 | 39 | execute_query("""CREATE TABLE green_energy ( 40 | time datetime, 41 | genW DECIMAL(9,3), 42 | conW DECIMAL(9,3), 43 | chgW DECIMAL(9,3), 44 | primary key(time) 45 | ); 46 | """, "twcmanager") 47 | 48 | execute_query("""CREATE TABLE slave_status ( 49 | slaveTWC varchar(4), 50 | time datetime, 51 | kWh int, 52 | voltsPhaseA int, 53 | voltsPhaseB int, 54 | voltsPhaseC int, 55 | primary key (slaveTWC, time) 56 | ); 57 | """, "twcmanager") 58 | -------------------------------------------------------------------------------- /tests/scripts/sqlite_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | 5 | def execute_query(query, database): 6 | 7 | queryb = query.encode('utf-8') 8 | process = subprocess.Popen(['sqlite3', database], 9 | stdout=subprocess.PIPE, 10 | stderr=subprocess.STDOUT, 11 | stdin=subprocess.PIPE) 12 | 13 | stdout, _ = process.communicate(input=queryb) 14 | 15 | if process.returncode != 0: 16 | print("Query failed: %s" % query) 17 | print("Output was: %s" % stdout) 18 | 19 | execute_query("""CREATE TABLE charge_sessions ( 20 | chargeid int, 21 | startTime datetime, 22 | startkWh int, 23 | slaveTWC varchar(4), 24 | endTime datetime, 25 | endkWh int, 26 | vehicleVIN varchar(17), 27 | primary key(startTime, slaveTWC) 28 | ); 29 | """, "/etc/twcmanager/twcmanager.sqlite") 30 | 31 | execute_query("""CREATE TABLE green_energy ( 32 | time datetime, 33 | genW DECIMAL(9,3), 34 | conW DECIMAL(9,3), 35 | chgW DECIMAL(9,3), 36 | primary key(time) 37 | ); 38 | """, "/etc/twcmanager/twcmanager.sqlite") 39 | 40 | execute_query("""CREATE TABLE slave_status ( 41 | slaveTWC varchar(4), 42 | time datetime, 43 | kWh int, 44 | voltsPhaseA int, 45 | voltsPhaseB int, 46 | voltsPhaseC int, 47 | primary key (slaveTWC, time) 48 | ); 49 | """, "/etc/twcmanager/twcmanager.sqlite") 50 | -------------------------------------------------------------------------------- /tests/scripts/upload_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################ 4 | # upload_file.sh 5 | # 6 | # This is a helper script for CI/CD testing run within dedicated TWCManager 7 | # test environments. It will upload a file to an upload server for analysis 8 | # should any strange behaviour need to be further reviewed. 9 | # 10 | # This would not be of much use outside of the dedicated test environment 11 | # 12 | 13 | FILE="$1" 14 | 15 | if [ "$FILE " == " " ]; then 16 | echo "No filename specified. Exiting." 17 | exit 0 18 | fi 19 | 20 | if [ ! -e "$FILE" ]; then 21 | echo "Specified file (${FILE}) doesn't exist. Exiting." 22 | exit 0 23 | fi 24 | 25 | HOSTNAME="`hostname -s`" 26 | FILEUNIQUE="`basename ${FILE}`.${HOSTNAME}.`date +%s`" 27 | SERVER=172.17.0.1 28 | TOKEN=0baa9000ff4ab70a6f9f89733438767a 29 | 30 | curl --proxy "" -X PUT -Ffile=@$FILE "http://${SERVER}:25478/files/${FILEUNIQUE}?token=${TOKEN}" 31 | --------------------------------------------------------------------------------