├── vwsfriend ├── tests │ ├── __init__.py │ ├── test_vwsfriend.py │ ├── test_location_util.py │ └── demos │ │ ├── hybrid_warning_light │ │ ├── 001_0s.cache.json │ │ └── 004_0s_light_off.cache.json │ │ ├── id_trip │ │ ├── 001_0s.cache.json │ │ ├── 002_0s_parking.cache.json │ │ ├── 003_0s_driving.cache.json │ │ └── 004_0s_parking2.cache.json │ │ ├── hybrid_maintenance │ │ ├── 001_0s.cache.json │ │ ├── 002_0s_drive.cache.json │ │ ├── 004_0s_drive2.cache.json │ │ └── 003_0s_all_maintenance.cache.json │ │ ├── hybrid_trip │ │ ├── 003_0s_driving.cache.json │ │ ├── 001_0s.cache.json │ │ ├── 002_0s_parking.cache.json │ │ └── 004_0s_parking2.cache.json │ │ ├── hybrid_refuel │ │ ├── 003_0s_refuelDone.cache.json │ │ ├── 001_0s.cache.json │ │ └── 002_0s_parking.cache.json │ │ ├── hybrid_refuel_position_before │ │ ├── 003_0s_refuelDone.cache.json │ │ ├── 001_0s.cache.json │ │ └── 002_0s_parking.cache.json │ │ └── hybrid_refuel_splitted │ │ ├── 003_0s_refuelPart.cache.json │ │ ├── 004_0s_refuelDone.cache.json │ │ ├── 001_0s.cache.json │ │ └── 002_0s_parking.cache.json ├── vwsfriend │ ├── __init__.py │ ├── agents │ │ ├── __init__.py │ │ ├── abrp │ │ │ └── __init__.py │ │ └── weconnect_error_agent.py │ ├── ui │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── static │ │ │ ├── icons │ │ │ │ ├── pin.png │ │ │ │ ├── abrp_icon.png │ │ │ │ └── settings.png │ │ │ └── style.css │ │ └── templates │ │ │ ├── database │ │ │ ├── select_vehicle.html │ │ │ ├── tag_list.html │ │ │ ├── operator_list.html │ │ │ ├── journey_list.html │ │ │ ├── geofence_list.html │ │ │ ├── backup.html │ │ │ ├── charger_list.html │ │ │ ├── trip_list.html │ │ │ ├── refuel_session_list.html │ │ │ ├── charging_session_list.html │ │ │ ├── operator_edit.html │ │ │ ├── overview.html │ │ │ ├── settings_edit.html │ │ │ ├── journey_edit.html │ │ │ └── tag_edit.html │ │ │ ├── restart.html │ │ │ ├── settings │ │ │ ├── vehicle.html │ │ │ ├── homekit.html │ │ │ └── abrpsettings.html │ │ │ ├── versions.html │ │ │ ├── login │ │ │ └── login.html │ │ │ ├── base.html │ │ │ └── status │ │ │ └── vehicles.html │ ├── util │ │ └── __init__.py │ ├── homekit │ │ ├── __init__.py │ │ ├── custom_characteristics.py │ │ ├── dummy_accessory.py │ │ ├── flashing.py │ │ ├── plug.py │ │ └── genericAccessory.py │ ├── model │ │ ├── vwsfriend-schema │ │ │ ├── __init__.py │ │ │ ├── versions │ │ │ │ ├── __init__.py │ │ │ │ ├── 90636c798967_initial.py │ │ │ │ ├── b117e50b5aa4_added_timezone_to_vehicle_settings.py │ │ │ │ ├── eb4c7c65c4fb_add_missing_enum.py │ │ │ │ ├── f3a66cd08ebe_add_additional_index_to_increase_query_.py │ │ │ │ ├── f69cf5eca1e2_add_mileage_to_warning_lights.py │ │ │ │ ├── aff91a2c4232_add_setting_for_order_and_hiding_of_.py │ │ │ │ ├── ae1799a66935_add_total_capacity_to_settings.py │ │ │ │ ├── 023aa9f36740_added_fields_for_real_kwh_and_cost.py │ │ │ │ ├── f6785099280a_added_attributes_for_real_refueled_.py │ │ │ │ ├── ffbb2fb8db2a_add_tire_enum.py │ │ │ │ ├── 1647742c9297_add_index_to_increase_query_performance.py │ │ │ │ ├── d13dfd852ab8_new_engine_category.py │ │ │ │ ├── 83cab0919846_add_missing_enums.py │ │ │ │ ├── f38c424891c2_new_enum_value_gasloline_cartype.py │ │ │ │ ├── 79ac0505ad35_add_tags_for_journey.py │ │ │ │ ├── 6c4cdc5006ba_added_battery_temperature_table.py │ │ │ │ ├── 06a5ec0a1af0_add_settings.py │ │ │ │ ├── 027f3fc22787_add_invalid_climatization_state.py │ │ │ │ ├── 91dac376210b_new_enum_values.py │ │ │ │ ├── 184bb7a71ada_added_error_table.py │ │ │ │ ├── a2ba1b877ee7_add_meta_information_for_charging_.py │ │ │ │ ├── f917fc564c1d_new_cartype.py │ │ │ │ ├── 689f13ef5c86_added_responsetime_table.py │ │ │ │ ├── 9a21960991c1_new_enum_values.py │ │ │ │ ├── f2fc2726507f_added_journey_table.py │ │ │ │ ├── 3500b84eb1f1_create_maintenance_table_and_add_.py │ │ │ │ ├── bd42a6f71742_added_warning_light_table.py │ │ │ │ ├── 901e3e466d03_add_changes_to_allow_geofencing_and_.py │ │ │ │ └── b7c0b285b1f3_add_tags.py │ │ │ ├── README │ │ │ ├── reference.db │ │ │ ├── script.py.mako │ │ │ └── env.py │ │ ├── base.py │ │ ├── tag.py │ │ ├── geofence.py │ │ ├── weconnect_error.py │ │ ├── weconnect_responsetime.py │ │ ├── __init__.py │ │ ├── online.py │ │ ├── datetime_decorator.py │ │ ├── migrations.py │ │ ├── vehicle_settings.py │ │ ├── settings.py │ │ ├── battery.py │ │ ├── battery_temperature.py │ │ ├── charger.py │ │ ├── climatization.py │ │ ├── journey.py │ │ ├── warning_light.py │ │ ├── maintenance.py │ │ ├── range.py │ │ ├── charge.py │ │ ├── refuel_session.py │ │ ├── trip.py │ │ └── vehicle.py │ ├── __version.py │ ├── __main__.py │ └── privacy.py ├── test_requirements.txt ├── mqtt_extra_requirements.txt ├── setup_requirements.txt ├── MANIFEST.in ├── pytest.ini ├── requirements.txt ├── .coveragerc ├── Makefile ├── setup.py ├── .gitignore ├── Dockerfile └── Dockerfile-edge ├── screenshots ├── id3.png ├── teaser.gif ├── homekit.jpg ├── homekit2.jpg ├── homekit3.jpg ├── homekit4.jpg ├── homekit5.jpg └── homekit6.jpg ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── grafana-vwsfriend-description.yml │ ├── grafana-dockerhub-description.yml │ ├── shellcheck.yml │ ├── compose.yml │ ├── build-vwsfriend-python.yml │ ├── vwsfriend-docker-edge.yml │ ├── vwsfriend-docker-experimental.yml │ ├── codeql-analysis.yml │ └── grafana-docker.yml ├── grafana ├── README.md ├── config │ └── grafana │ │ └── provisioning │ │ ├── datasources │ │ ├── vwsfriend-live.yml │ │ └── vwsfriend.yml │ │ └── dashboards │ │ └── vwsfriend.yml ├── Dockerfile └── public │ └── img │ └── icons │ ├── gas.svg │ ├── parking.svg │ ├── charger.svg │ └── car.svg ├── .env └── LICENSE /vwsfriend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/agents/abrp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/test_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/__version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.0dev' 2 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vwsfriend/mqtt_extra_requirements.txt: -------------------------------------------------------------------------------- 1 | weconnect-mqtt~=0.49.2 2 | -------------------------------------------------------------------------------- /vwsfriend/tests/test_vwsfriend.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_dummy(): 4 | assert True 5 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /screenshots/id3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/id3.png -------------------------------------------------------------------------------- /screenshots/teaser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/teaser.gif -------------------------------------------------------------------------------- /screenshots/homekit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit.jpg -------------------------------------------------------------------------------- /screenshots/homekit2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit2.jpg -------------------------------------------------------------------------------- /screenshots/homekit3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit3.jpg -------------------------------------------------------------------------------- /screenshots/homekit4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit4.jpg -------------------------------------------------------------------------------- /screenshots/homekit5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit5.jpg -------------------------------------------------------------------------------- /screenshots/homekit6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/screenshots/homekit6.jpg -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/cache.py: -------------------------------------------------------------------------------- 1 | from flask_caching import Cache 2 | 3 | cache = Cache(config={'CACHE_TYPE': 'simple'}) 4 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/__main__.py: -------------------------------------------------------------------------------- 1 | from vwsfriend.vwsfriend_base import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | myconfig.env 2 | docker-compose-homekit-macvlan.override.yml 3 | docker-compose-homekit-macvlan-dev.yml 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /vwsfriend/setup_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-runner~=6.0.1 2 | mccabe~=0.7.0 3 | flake8~=7.1.1 4 | pylint~=3.2.7 5 | bandit~=1.7.9 6 | -------------------------------------------------------------------------------- /vwsfriend/MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft vwsfriend/ui/static 2 | graft vwsfriend/ui/templates 3 | graft vwsfriend/model/alembic.ini 4 | global-exclude *.pyc -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/static/icons/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/vwsfriend/vwsfriend/ui/static/icons/pin.png -------------------------------------------------------------------------------- /vwsfriend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | addopts = --cov=vwsfriend --cov-config=.coveragerc --cov-report html 4 | required_plugins = pytest-cov -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/static/icons/abrp_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/vwsfriend/vwsfriend/ui/static/icons/abrp_icon.png -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/static/icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/vwsfriend/vwsfriend/ui/static/icons/settings.png -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/reference.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillsteinbach/VWsFriend/HEAD/vwsfriend/vwsfriend/model/vwsfriend-schema/reference.db -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/privacy.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Privacy(Enum): 5 | NO_LOCATIONS = 'no-locations' 6 | 7 | def __str__(self): 8 | return self.value 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "pip" 9 | directory: "/vwsfriend/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /grafana/README.md: -------------------------------------------------------------------------------- 1 | # Grafana provisioned for VWsFriend Software 2 | This image provisions Grafana with the following: 3 | - datasource to connect to influxdb with data from the car 4 | - dashboards visualizing data from the car 5 | 6 | ## More information 7 | More information can be found on Github: https://github.com/tillsteinbach/VWsFriend/ 8 | -------------------------------------------------------------------------------- /vwsfriend/requirements.txt: -------------------------------------------------------------------------------- 1 | weconnect[Images]~=0.60.10 2 | HAP-python[QRCode]~=4.9.1 3 | pypng~=0.20220715.0 4 | sqlalchemy~=2.0.34 5 | psycopg2-binary~=2.9.9 6 | requests~=2.32.3 7 | Werkzeug~=3.0.4 8 | Flask~=3.0.3 9 | flask-login~=0.6.3 10 | flask-caching~=2.3.0 11 | WTForms~=3.1.2 12 | flask_wtf~=1.2.1 13 | flask_sqlalchemy~=3.1.1 14 | alembic~=1.13.2 15 | haversine~=2.8.1 16 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/custom_characteristics.py: -------------------------------------------------------------------------------- 1 | CUSTOM_CHARACTERISTICS = { 2 | "Consumption": { 3 | "Format": "uint32", 4 | "Permissions": [ 5 | "pr", 6 | "ev" 7 | ], 8 | "UUID": "E863F10D-079E-48FF-8F27-9C2605A29F52", 9 | "maxValue": 4294967295, 10 | "minStep": 1, 11 | "minValue": 0, 12 | "unit": "watt" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/select_vehicle.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Select vehicle to proceed{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 13 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/restart.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 9 | 10 | {% endblock %} 11 | 12 | {% block header %} 13 |

{% block title %}Restart{% endblock %}

14 | {% endblock %} 15 | 16 | {% block content %} 17 | Restart is in progress... 18 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/90636c798967_initial.py: -------------------------------------------------------------------------------- 1 | """initial 2 | 3 | Revision ID: 90636c798967 4 | Revises: 5 | Create Date: 2021-09-06 14:06:46.975495 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '90636c798967' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/dummy_accessory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pyhap.accessory import Accessory 4 | from pyhap.const import CATEGORY_OTHER 5 | 6 | LOG = logging.getLogger("VWsFriend") 7 | 8 | 9 | class DummyAccessory(Accessory): 10 | 11 | category = CATEGORY_OTHER 12 | 13 | def __init__(self, driver, aid, displayName): 14 | super().__init__(driver=driver, display_name=displayName, aid=aid) 15 | 16 | @Accessory.available.getter 17 | def available(self): 18 | return False 19 | -------------------------------------------------------------------------------- /vwsfriend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | exclude_lines = 6 | # Have to re-enable the standard pragma 7 | pragma: no cover 8 | 9 | # Don't complain about missing debug-only code: 10 | def __repr__ 11 | if self\.debug 12 | 13 | # Don't complain if tests don't hit defensive assertion code: 14 | raise AssertionError 15 | raise NotImplementedError 16 | 17 | # Don't complain if non-runnable code isn't run: 18 | if 0: 19 | if __name__ == .__main__.: 20 | 21 | [html] 22 | directory = coverage_html_report -------------------------------------------------------------------------------- /vwsfriend/Makefile: -------------------------------------------------------------------------------- 1 | MODULE := vwsfriend.vwsfriend 2 | BLUE='\033[0;34m' 3 | NC='\033[0m' # No Color 4 | 5 | run: 6 | @python -m $(MODULE) 7 | 8 | test: 9 | @pytest 10 | 11 | lint: 12 | @echo "\n${BLUE}Running Pylint against source and test files...${NC}\n" 13 | @pylint --rcfile=setup.cfg **/*.py 14 | @echo "\n${BLUE}Running Flake8 against source and test files...${NC}\n" 15 | @flake8 16 | @echo "\n${BLUE}Running Bandit against source files...${NC}\n" 17 | @bandit -r --ini setup.cfg 18 | 19 | clean: 20 | rm -rf .pytest_cache .coverage .pytest_cache coverage.xml coverage_html_report 21 | 22 | .PHONY: clean test -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}All tags in database{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | {% for tag in tags %} 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 |
NameDescription
{{ tag.name }}{{ tag.description }}
20 | {% endblock %} -------------------------------------------------------------------------------- /grafana/config/grafana/provisioning/datasources/vwsfriend-live.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | deleteDatasources: 5 | - name: VWsFriend Live 6 | orgId: 1 7 | 8 | datasources: 9 | - name: VWsFriend Live 10 | orgId: 1 11 | uid: P36D08C1A0297C737 12 | type: marcusolsson-json-datasource 13 | access: proxy 14 | url: http://$VWSFRIEND_HOSTNAME:$VWSFRIEND_PORT 15 | basicAuthUser: $VWSFRIEND_USERNAME 16 | secureJsonData: 17 | basicAuthPassword: $VWSFRIEND_PASSWORD 18 | jsonData: 19 | timeout: "1" 20 | basicAuth: true 21 | isDefault: false 22 | editable: false 23 | 24 | -------------------------------------------------------------------------------- /grafana/config/grafana/provisioning/datasources/vwsfriend.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | deleteDatasources: 5 | - name: VWsFriend 6 | orgId: 1 7 | 8 | datasources: 9 | - name: VWsFriend 10 | orgId: 1 11 | uid: P2EF847825A020B66 12 | type: postgres 13 | access: proxy 14 | database: $DB_NAME 15 | user: $DB_USER 16 | secureJsonData: 17 | password: $DB_PASSWORD 18 | jsonData: 19 | sslmode: "disable" 20 | postgresVersion: 1200 21 | url: $DB_HOSTNAME:$DB_PORT 22 | basicAuth: false 23 | withCredentials: false 24 | isDefault: true 25 | editable: false 26 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/operator_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}All custom created operators in database{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | {% for operator in operators %} 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 |
NamePhone
{{ operator.name }}{{ operator.phone }}
20 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Boolean 2 | 3 | from vwsfriend.model.base import Base 4 | 5 | 6 | class Tag(Base): 7 | __tablename__ = 'tag' 8 | name = Column(String, primary_key=True) 9 | description = Column(String, nullable=True) 10 | use_trips = Column(Boolean, default=False, nullable=False) 11 | use_charges = Column(Boolean, default=False, nullable=False) 12 | use_refueling = Column(Boolean, default=False, nullable=False) 13 | use_journey = Column(Boolean, default=False, nullable=False) 14 | 15 | def __init__(self, name, description=None): 16 | self.name = name 17 | self.description = description 18 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/b117e50b5aa4_added_timezone_to_vehicle_settings.py: -------------------------------------------------------------------------------- 1 | """Added timezone to vehicle settings 2 | 3 | Revision ID: b117e50b5aa4 4 | Revises: 3500b84eb1f1 5 | Create Date: 2022-05-24 10:33:35.268307 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b117e50b5aa4' 14 | down_revision = '3500b84eb1f1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('vehicle_settings', sa.Column('timezone', sa.String(length=256), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('vehicle_settings', 'timezone') 25 | 26 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/eb4c7c65c4fb_add_missing_enum.py: -------------------------------------------------------------------------------- 1 | """Add missing ENUM 2 | 3 | Revision ID: eb4c7c65c4fb 4 | Revises: 1b71c67dd2eb 5 | Create Date: 2022-02-04 20:04:08.442697 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'eb4c7c65c4fb' 14 | down_revision = '1b71c67dd2eb' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE chargingstate ADD VALUE IF NOT EXISTS 'CONSERVATION'") 23 | 24 | 25 | def downgrade(): 26 | pass 27 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/geofence.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Float, Integer, BigInteger, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | 6 | 7 | class Geofence(Base): 8 | __tablename__ = 'geofences' 9 | id = Column(Integer, primary_key=True) 10 | name = Column(String) 11 | latitude = Column(Float) 12 | longitude = Column(Float) 13 | radius = Column(Float) 14 | location_id = Column(BigInteger, ForeignKey('locations.osm_id')) 15 | location = relationship("Location") 16 | charger_id = Column(String, ForeignKey('chargers.id')) 17 | charger = relationship("Charger") 18 | 19 | def __init__(self, id): 20 | self.id = id 21 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/weconnect_error.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Enum 2 | 3 | from vwsfriend.model.base import Base 4 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 5 | 6 | from weconnect.weconnect_errors import ErrorEventType 7 | 8 | 9 | class WeConnectError(Base): 10 | __tablename__ = 'weconnect_errors' 11 | id = Column(Integer, primary_key=True) 12 | datetime = Column(DatetimeDecorator(timezone=True), nullable=False) 13 | errortype = Column(Enum(ErrorEventType)) 14 | detail = Column(String) 15 | 16 | def __init__(self, datetime, errortype, detail): 17 | self.datetime = datetime 18 | self.errortype = errortype 19 | self.detail = detail 20 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/weconnect_responsetime.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Float 2 | 3 | from vwsfriend.model.base import Base 4 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 5 | 6 | 7 | class WeConnectResponsetime(Base): 8 | __tablename__ = 'weconnect_responsetime' 9 | id = Column(Integer, primary_key=True) 10 | datetime = Column(DatetimeDecorator(timezone=True), nullable=False) 11 | min = Column(Float) 12 | avg = Column(Float) 13 | max = Column(Float) 14 | total = Column(Float) 15 | 16 | def __init__(self, datetime, min, avg, max, total): 17 | self.datetime = datetime 18 | self.min = min 19 | self.avg = avg 20 | self.max = max 21 | self.total = total 22 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f3a66cd08ebe_add_additional_index_to_increase_query_.py: -------------------------------------------------------------------------------- 1 | """add additional index to increase query performance 2 | 3 | Revision ID: f3a66cd08ebe 4 | Revises: 1647742c9297 5 | Create Date: 2023-03-01 16:07:55.295749 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f3a66cd08ebe' 14 | down_revision = '1647742c9297' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_index('charges_idx_vehicle_vin_chargepower_kw', 'charges', ['vehicle_vin', 'chargePower_kW'], unique=False) 21 | 22 | 23 | def downgrade(): 24 | op.drop_index('charges_idx_vehicle_vin_chargepower_kw', table_name='charges') 25 | -------------------------------------------------------------------------------- /.github/workflows/grafana-vwsfriend-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description for VWsFriend 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - vwsfriend/README.md 8 | - .github/workflows/grafana-vwsfriend-description.yml 9 | jobs: 10 | dockerHubDescription: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Docker Hub Description 16 | uses: peter-evans/dockerhub-description@v4.0.0 17 | with: 18 | username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 20 | repository: tillsteinbach/vwsfriend 21 | short-description: "VWsFriend Software. See: https://github.com/tillsteinbach/VWsFriend/" 22 | readme-filepath: ./README.md 23 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/journey_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}All journeys in database{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for journey in journeys %} 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |
TitleDescriptionVehicle
{{ journey.title }}{{ journey.description }}{{ journey.vehicle.displayString() }}
22 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/geofence_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}All geofences in database{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for geofence in geofences %} 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |
IDNameCharger
{{ geofence.id }}{{ geofence.name }}{% if geofence.charger %}{{ geofence.charger.name}}{% endif %}
22 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/settings/vehicle.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Vehicle Settings for {{vehicle.nickname.value}}{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 19 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f69cf5eca1e2_add_mileage_to_warning_lights.py: -------------------------------------------------------------------------------- 1 | """add mileage to warning lights 2 | 3 | Revision ID: f69cf5eca1e2 4 | Revises: bd42a6f71742 5 | Create Date: 2022-04-08 21:56:59.115989 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f69cf5eca1e2' 14 | down_revision = 'bd42a6f71742' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('warning_lights', sa.Column('start_mileage', sa.Integer(), nullable=True)) 21 | op.add_column('warning_lights', sa.Column('end_mileage', sa.Integer(), nullable=True)) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('warning_lights', 'end_mileage') 26 | op.drop_column('warning_lights', 'start_mileage') 27 | -------------------------------------------------------------------------------- /.github/workflows/grafana-dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description for Grafana 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - grafana/README.md 8 | - .github/workflows/grafana-dockerhub-description.yml 9 | jobs: 10 | dockerHubDescription: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Docker Hub Description 16 | uses: peter-evans/dockerhub-description@v4.0.0 17 | with: 18 | username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 20 | repository: tillsteinbach/vwsfriend-grafana 21 | short-description: "Grafana provisioned for VWsFriend Software. See: https://github.com/tillsteinbach/VWsFriend/" 22 | readme-filepath: ./grafana/README.md 23 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/aff91a2c4232_add_setting_for_order_and_hiding_of_.py: -------------------------------------------------------------------------------- 1 | """Add setting for order and hiding of vehicles 2 | 3 | Revision ID: aff91a2c4232 4 | Revises: f917fc564c1d 5 | Create Date: 2022-09-05 11:33:54.780537 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'aff91a2c4232' 14 | down_revision = 'f917fc564c1d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('vehicle_settings', sa.Column('sorting_order', sa.Integer(), nullable=True)) 21 | op.add_column('vehicle_settings', sa.Column('hide', sa.Boolean(), nullable=True)) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('vehicle_settings', 'hide') 26 | op.drop_column('vehicle_settings', 'sorting_order') 27 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/backup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Database Backup & Restore{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% if form %} 9 |
10 |
11 | {{ form.csrf_token }} 12 | 13 |
14 | {{ form.file.label }} 15 | {{ form.file }} 16 | {% if form.file.errors %} 17 |
    18 | {% for error in form.file.errors %} 19 |
  • {{ error }}
  • 20 | {% endfor %} 21 |
22 | {% endif %} 23 |
24 | 25 | {{ form.backup }} 26 | {{ form.restore }} 27 |
28 |
29 | {% endif %} 30 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/charger_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}All custom created charging stations in database{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for charger in chargers %} 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |
NameAddressOperator
{{ charger.name }}{{ charger.address }}{% if charger.operator %}{{ charger.operator.displayString() }}{% endif %}
22 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .battery import Battery 2 | from .battery_temperature import BatteryTemperature 3 | from .charge import Charge 4 | from .charging_session import ChargingSession 5 | from .climatization import Climatization 6 | from .journey import Journey 7 | from .location import Location 8 | from .geofence import Geofence 9 | from .online import Online 10 | from .range import Range 11 | from .refuel_session import RefuelSession 12 | from .trip import Trip 13 | from .vehicle_settings import VehicleSettings 14 | from .vehicle import Vehicle 15 | from .charger import Charger, Operator 16 | from .settings import Settings 17 | from .weconnect_error import WeConnectError 18 | from .weconnect_responsetime import WeConnectResponsetime 19 | from .tag import Tag 20 | from .warning_light import WarningLight 21 | from .maintenance import Maintenance 22 | -------------------------------------------------------------------------------- /grafana/config/grafana/provisioning/dashboards/vwsfriend.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | # an unique provider name. Required 5 | - name: 'vwsfriend' 6 | # Org id. Default to 1 7 | orgId: 1 8 | # provider type. Default to 'file' 9 | type: file 10 | # disable dashboard deletion 11 | disableDeletion: true 12 | # how often Grafana will scan for changed dashboards 13 | updateIntervalSeconds: 60 14 | # allow updating provisioned dashboards from the UI 15 | allowUiUpdates: false 16 | options: 17 | # path to dashboard files on disk. Required when using the 'file' type 18 | path: /var/lib/grafana-static/dashboards/vwsfriend 19 | # use folder names from filesystem to create folders in Grafana 20 | foldersFromFilesStructure: true 21 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/online.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | class Online(Base): 9 | __tablename__ = 'onlinestates' 10 | __table_args__ = ( 11 | UniqueConstraint('vehicle_vin', 'onlineTime'), 12 | ) 13 | id = Column(Integer, primary_key=True) 14 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 15 | onlineTime = Column(DatetimeDecorator) 16 | offlineTime = Column(DatetimeDecorator) 17 | vehicle = relationship("Vehicle") 18 | 19 | def __init__(self, vehicle, onlineTime, offlineTime): 20 | self.vehicle = vehicle 21 | self.onlineTime = onlineTime 22 | self.offlineTime = offlineTime 23 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/datetime_decorator.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | import sqlalchemy 3 | 4 | 5 | class DatetimeDecorator(sqlalchemy.types.TypeDecorator): 6 | impl = sqlalchemy.types.DateTime 7 | cache_ok = True 8 | 9 | LOCAL_TIMEZONE = datetime.utcnow().astimezone().tzinfo 10 | 11 | def process_bind_param(self, value: datetime, dialect): 12 | if value is None: 13 | return value 14 | 15 | if value.tzinfo is None: 16 | value = value.astimezone(self.LOCAL_TIMEZONE) 17 | 18 | return value.astimezone(timezone.utc) 19 | 20 | def process_result_value(self, value, dialect): 21 | if value is None: 22 | return value 23 | 24 | if value.tzinfo is None: 25 | return value.replace(tzinfo=timezone.utc) 26 | 27 | return value.astimezone(timezone.utc) 28 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/migrations.py: -------------------------------------------------------------------------------- 1 | from alembic.command import upgrade, stamp 2 | from alembic.config import Config 3 | import os 4 | 5 | 6 | def run_database_migrations(dsn: str, stampOnly: bool = False): 7 | # retrieves the directory that *this* file is in 8 | migrations_dir = os.path.dirname(os.path.realpath(__file__)) 9 | # this assumes the alembic.ini is also contained in this same directory 10 | config_file = os.path.join(migrations_dir, "alembic.ini") 11 | 12 | config = Config(file_=config_file) 13 | config.attributes['configure_logger'] = False 14 | config.set_main_option("script_location", migrations_dir + '/vwsfriend-schema') 15 | config.set_main_option('sqlalchemy.url', dsn) 16 | 17 | if stampOnly: 18 | stamp(config, "head") 19 | else: 20 | # upgrade the database to the latest revision 21 | upgrade(config, "head") 22 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/ae1799a66935_add_total_capacity_to_settings.py: -------------------------------------------------------------------------------- 1 | """Add total capacity to settings 2 | 3 | Revision ID: ae1799a66935 4 | Revises: 901e3e466d03 5 | Create Date: 2022-02-18 09:57:06.622149 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ae1799a66935' 14 | down_revision = '901e3e466d03' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('vehicle_settings', sa.Column('primary_capacity_total', sa.Integer(), nullable=True)) 21 | op.add_column('vehicle_settings', sa.Column('secondary_capacity_total', sa.Integer(), nullable=True)) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('vehicle_settings', 'secondary_capacity_total') 26 | op.drop_column('vehicle_settings', 'primary_capacity_total') 27 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vehicle_settings.py: -------------------------------------------------------------------------------- 1 | 2 | from sqlalchemy import Column, Integer, String, Boolean, ForeignKey 3 | from sqlalchemy.orm import relationship 4 | 5 | from vwsfriend.model.base import Base 6 | 7 | 8 | class VehicleSettings(Base): 9 | __tablename__ = 'vehicle_settings' 10 | vehicle_vin = Column(String, ForeignKey('vehicles.vin'), primary_key=True) 11 | vehicle = relationship("Vehicle", back_populates="settings") 12 | 13 | primary_capacity = Column(Integer) 14 | primary_capacity_total = Column(Integer) 15 | primary_wltp_range = Column(Integer) 16 | secondary_capacity = Column(Integer) 17 | secondary_capacity_total = Column(Integer) 18 | secondary_wltp_range = Column(Integer) 19 | timezone = Column(String(256)) 20 | sorting_order = Column(Integer) 21 | hide = Column(Boolean) 22 | 23 | def __init__(self, vehicle): 24 | self.vehicle = vehicle 25 | -------------------------------------------------------------------------------- /vwsfriend/tests/test_location_util.py: -------------------------------------------------------------------------------- 1 | 2 | # from vwsfriend.util.location_util import locationFromLatLon 3 | 4 | def test_locationFromLatLon(): 5 | # location = locationFromLatLon(latitude=53.60853453781239, longitude=10.19093520255688) 6 | # print (location) 7 | # assert location is not None 8 | 9 | # location = locationFromLatLon(latitude=53.60957750612437, longitude=10.184564007135274) 10 | # print (location) 11 | # assert location is not None 12 | 13 | # location = locationFromLatLon(latitude=53.61014, longitude=10.18435) 14 | # print (location) 15 | # assert location is not None 16 | 17 | # location = locationFromLatLon(latitude=53.66243, longitude=10.16044) 18 | # print (location) 19 | # assert location is not None 20 | 21 | # location = locationFromLatLon(latitude=53.65190, longitude=10.16269) 22 | # print (location) 23 | # assert location is not None 24 | assert True 25 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/settings.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from sqlalchemy import Column, Integer, String, Enum 4 | 5 | from vwsfriend.model.base import Base 6 | 7 | 8 | class UnitOfLength(enum.Enum): 9 | KM = 'km' 10 | MI = 'mi' 11 | 12 | 13 | class UnitOfTemperature(enum.Enum): 14 | C = 'C' 15 | F = 'F' 16 | 17 | 18 | class Settings(Base): 19 | __tablename__ = 'settings' 20 | id = Column(Integer, primary_key=True) 21 | unit_of_length = Column(Enum(UnitOfLength)) 22 | unit_of_temperature = Column(Enum(UnitOfTemperature)) 23 | grafana_url = Column(String) 24 | vwsfriend_url = Column(String) 25 | locale = Column(String) 26 | 27 | def __init__(self, grafana_url, vwsfriend_url): 28 | self.unit_of_length = UnitOfLength.KM 29 | self.unit_of_temperature = UnitOfTemperature.C 30 | self.grafana_url = grafana_url 31 | self.vwsfriend_url = vwsfriend_url 32 | self.locale = 'en_US' 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/versions.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}About{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |

VWsFriend is developed under the MIT open-source license.

9 | 10 |

Donations

11 |

VWsFriend is free for everyone. If you think VWsFriend is usefull please consider donating through

12 |
    13 |
  • PayPal
  • 14 |
  • GitHub Sponsors
  • 15 | 16 |

    A big thanks to all sponsors for their support. With your contribution you make free software possible!

    17 | 18 |

    Versions

    19 |
      20 | {% for tool, version in g.versions.items() %} 21 |
    • {{tool}}: {{version}}
    • 22 | {% endfor %} 23 |
    24 | {% endblock %} -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: ShellCheck 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | paths: 11 | - '**.sh' 12 | pull_request: 13 | branches: [ main ] 14 | paths: 15 | - '**.sh' 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | jobs: 21 | shellcheck: 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-latest 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | 30 | # Runs ShellCheck 31 | - name: ShellCheck 32 | uses: ludeeus/action-shellcheck@2.0.0 33 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VWSFRIEND_USERNAME='admin' 2 | VWSFRIEND_PASSWORD='secret' # For security reasons please change the default password and remove this comment! 3 | WECONNECT_USER='test@test.de' 4 | WECONNECT_PASSWORD='secret' 5 | WECONNECT_SPIN= # Pin is only necessary for locking/unlocking in Homekit. If you don't want this keep WECONNECT_PIN empty 6 | WECONNECT_INTERVAL=300 7 | DB_USER='admin' 8 | DB_PASSWORD='secret' # For security reasons please change the default password and remove this comment! 9 | ADDITIONAL_PARAMETERS=-vv 10 | 11 | # If you don't want to use Apple Homekit you can ignore these settings! 12 | # The following settings are only used with homekit in macvlan mode! 13 | # Set HOMEKIT_IP to a free IP address in your network 14 | # Configure HOMEKIT_MASK and HOMEKIT_GW with the correct settings for your network 15 | # In macvlan mode you reach the VWsFriend UI through the configured HOMEKIT_IP on port 4000 16 | HOMEKIT_INTERFACE=eth0 17 | HOMEKIT_IP=192.168.0.234 18 | HOMEKIT_MASK=192.168.0.0/24 19 | HOMEKIT_GW=192.168.0.1 20 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/023aa9f36740_added_fields_for_real_kwh_and_cost.py: -------------------------------------------------------------------------------- 1 | """Added fields for real kwh and cost 2 | 3 | Revision ID: 023aa9f36740 4 | Revises: 184bb7a71ada 5 | Create Date: 2021-09-28 16:24:00.035078 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '023aa9f36740' 14 | down_revision = '184bb7a71ada' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('charging_sessions', sa.Column('realCharged_kWh', sa.Float(), nullable=True)) 22 | op.add_column('charging_sessions', sa.Column('realCost_ct', sa.Integer(), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('charging_sessions', 'realCost_ct') 29 | op.drop_column('charging_sessions', 'realCharged_kWh') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f6785099280a_added_attributes_for_real_refueled_.py: -------------------------------------------------------------------------------- 1 | """Added attributes for real refueled amount and cost 2 | 3 | Revision ID: f6785099280a 4 | Revises: f2fc2726507f 5 | Create Date: 2021-10-11 21:26:49.789401 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f6785099280a' 14 | down_revision = 'f2fc2726507f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('refuel_sessions', sa.Column('realRefueled_l', sa.Float(), nullable=True)) 22 | op.add_column('refuel_sessions', sa.Column('realCost_ct', sa.Integer(), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('refuel_sessions', 'realCost_ct') 29 | op.drop_column('refuel_sessions', 'realRefueled_l') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/ffbb2fb8db2a_add_tire_enum.py: -------------------------------------------------------------------------------- 1 | """Add TIRE Enum 2 | 3 | Revision ID: ffbb2fb8db2a 4 | Revises: aff91a2c4232 5 | Create Date: 2022-10-13 14:06:43.668308 6 | 7 | """ 8 | from alembic import op 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'ffbb2fb8db2a' 13 | down_revision = 'aff91a2c4232' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | if op.get_context().dialect.name == 'postgresql': 20 | with op.get_context().autocommit_block(): 21 | op.execute("ALTER TYPE category ADD VALUE IF NOT EXISTS 'TIRE'") 22 | 23 | 24 | def downgrade(): 25 | if op.get_context().dialect.name == 'postgresql': 26 | op.execute("ALTER TYPE category RENAME TO category_old") 27 | op.execute("CREATE TYPE category AS ENUM('LIGHTING', 'UNKNOWN')") 28 | op.execute(( 29 | "ALTER TABLE transactions ALTER COLUMN category TYPE category USING " 30 | "category::text::category" 31 | )) 32 | op.execute("DROP TYPE category_old") 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/battery.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | class Battery(Base): 9 | __tablename__ = 'battery' 10 | __table_args__ = ( 11 | UniqueConstraint('vehicle_vin', 'carCapturedTimestamp'), 12 | ) 13 | id = Column(Integer, primary_key=True) 14 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 15 | carCapturedTimestamp = Column(DatetimeDecorator(timezone=True), nullable=False) 16 | vehicle = relationship("Vehicle") 17 | currentSOC_pct = Column(Integer) 18 | cruisingRangeElectric_km = Column(Integer) 19 | 20 | def __init__(self, vehicle, carCapturedTimestamp, currentSOC_pct, cruisingRangeElectric_km): 21 | self.vehicle = vehicle 22 | self.carCapturedTimestamp = carCapturedTimestamp 23 | self.currentSOC_pct = currentSOC_pct 24 | self.cruisingRangeElectric_km = cruisingRangeElectric_km 25 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/1647742c9297_add_index_to_increase_query_performance.py: -------------------------------------------------------------------------------- 1 | """add index to increase query performance 2 | 3 | Revision ID: 1647742c9297 4 | Revises: a2ba1b877ee7 5 | Create Date: 2023-03-01 14:30:31.836580 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1647742c9297' 14 | down_revision = 'a2ba1b877ee7' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_index(op.f('ix_charges_chargePower_kW'), 'charges', ['chargePower_kW'], unique=False) 21 | op.create_index(op.f('ix_charges_chargingState'), 'charges', ['chargingState'], unique=False) 22 | op.create_index(op.f('ix_charging_sessions_acdc'), 'charging_sessions', ['acdc'], unique=False) 23 | 24 | def downgrade(): 25 | op.drop_index(op.f('ix_charging_sessions_acdc'), table_name='charging_sessions') 26 | op.drop_index(op.f('ix_charges_chargingState'), table_name='charges') 27 | op.drop_index(op.f('ix_charges_chargePower_kW'), table_name='charges') 28 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/d13dfd852ab8_new_engine_category.py: -------------------------------------------------------------------------------- 1 | """New ENGINE category 2 | 3 | Revision ID: d13dfd852ab8 4 | Revises: ffbb2fb8db2a 5 | Create Date: 2023-02-20 10:27:41.958140 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd13dfd852ab8' 14 | down_revision = 'ffbb2fb8db2a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE category ADD VALUE IF NOT EXISTS 'ENGINE'") 23 | 24 | def downgrade(): 25 | if op.get_context().dialect.name == 'postgresql': 26 | op.execute("ALTER TYPE category RENAME TO category_old") 27 | op.execute("CREATE TYPE category AS ENUM('LIGHTING', 'TIRE', 'UNKNOWN')") 28 | op.execute(( 29 | "ALTER TABLE transactions ALTER COLUMN category TYPE category USING " 30 | "category::text::category" 31 | )) 32 | op.execute("DROP TYPE category_old") 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/83cab0919846_add_missing_enums.py: -------------------------------------------------------------------------------- 1 | """add missing Enums 2 | 3 | Revision ID: 83cab0919846 4 | Revises: 6c4cdc5006ba 5 | Create Date: 2024-09-13 08:06:20.121117 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '83cab0919846' 14 | down_revision = '6c4cdc5006ba' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE category ADD VALUE IF NOT EXISTS 'OTHER'") 23 | 24 | 25 | def downgrade(): 26 | if op.get_context().dialect.name == 'postgresql': 27 | op.execute("ALTER TYPE category RENAME TO category_old") 28 | op.execute("CREATE TYPE category AS ENUM('ENGINE', 'LIGHTING', 'TIRE', 'UNKNOWN')") 29 | op.execute(( 30 | "ALTER TABLE transactions ALTER COLUMN category TYPE category USING " 31 | "category::text::category" 32 | )) 33 | op.execute("DROP TYPE category_old") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Till Steinbach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f38c424891c2_new_enum_value_gasloline_cartype.py: -------------------------------------------------------------------------------- 1 | """New enum value gasloline carType 2 | 3 | Revision ID: f38c424891c2 4 | Revises: 91dac376210b 5 | Create Date: 2021-12-08 12:31:41.706156 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f38c424891c2' 14 | down_revision = '91dac376210b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE cartype ADD VALUE 'GASOLINE'") 23 | 24 | 25 | def downgrade(): 26 | if op.get_context().dialect.name == 'postgresql': 27 | op.execute("ALTER TYPE cartype RENAME TO cartype_old") 28 | op.execute("CREATE TYPE cartype AS ENUM('ELECTRIC', 'HYBRID', 'UNKNOWN')") 29 | op.execute(( 30 | "ALTER TABLE transactions ALTER COLUMN cartype TYPE cartype USING " 31 | "cartype::text::cartype" 32 | )) 33 | op.execute("DROP TYPE cartype_old") 34 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/trip_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}All trips in database{% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for trip in trips %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 |
    IDStart DateStart LocationEnd LocationVehicle
    {{ trip.id }}{{ trip.startDate }}{% if trip.start_location %}{{ trip.start_location.displayString()}}{% endif %}{% if trip.destination_location %}{{ trip.destination_location.displayString()}}{% endif %}{{ trip.vehicle.displayString() }}
    26 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/battery_temperature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Float, String, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | class BatteryTemperature(Base): 9 | __tablename__ = 'battery_temperature' 10 | __table_args__ = ( 11 | UniqueConstraint('vehicle_vin', 'carCapturedTimestamp'), 12 | ) 13 | id = Column(Integer, primary_key=True) 14 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 15 | carCapturedTimestamp = Column(DatetimeDecorator(timezone=True), nullable=False) 16 | vehicle = relationship("Vehicle") 17 | temperatureHvBatteryMin_K = Column(Float) 18 | temperatureHvBatteryMax_K = Column(Float) 19 | 20 | def __init__(self, vehicle, carCapturedTimestamp, temperatureHvBatteryMin_K, temperatureHvBatteryMax_K): 21 | self.vehicle = vehicle 22 | self.carCapturedTimestamp = carCapturedTimestamp 23 | self.temperatureHvBatteryMin_K = temperatureHvBatteryMin_K 24 | self.temperatureHvBatteryMax_K = temperatureHvBatteryMax_K 25 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/79ac0505ad35_add_tags_for_journey.py: -------------------------------------------------------------------------------- 1 | """Add tags for journey 2 | 3 | Revision ID: 79ac0505ad35 4 | Revises: b7c0b285b1f3 5 | Create Date: 2022-02-21 17:04:29.272843 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '79ac0505ad35' 14 | down_revision = 'b7c0b285b1f3' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('journey_tag', 21 | sa.Column('journey_id', sa.Integer(), nullable=True), 22 | sa.Column('tag_name', sa.String(), nullable=True), 23 | sa.ForeignKeyConstraint(['journey_id'], ['journey.id'], ), 24 | sa.ForeignKeyConstraint(['tag_name'], ['tag.name'], ) 25 | ) 26 | op.add_column('tag', sa.Column('use_journey', sa.Boolean(), nullable=True)) 27 | op.execute("UPDATE tag SET use_journey = false") 28 | op.alter_column('tag', 'use_journey', nullable=False) 29 | 30 | 31 | def downgrade(): 32 | op.drop_column('tag', 'use_journey') 33 | op.drop_table('journey_tag') 34 | -------------------------------------------------------------------------------- /grafana/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grafana/grafana:11.1.5 2 | 3 | ENV GF_LOG_LEVEL=info 4 | ENV GF_SECURITY_ADMIN_USER=admin 5 | ENV GF_SECURITY_ADMIN_PASSWORD=admin 6 | ENV GF_USERS_ALLOW_SIGN_UP=false 7 | ENV GF_INSTALL_PLUGINS="marcusolsson-json-datasource,gapit-htmlgraphics-panel" 8 | ENV GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS= 9 | ENV GF_PLUGINS_ENABLE_ALPHA=true 10 | ENV GF_SERVER_ENABLE_GZIP=true 11 | ENV DB_USER=admin 12 | ENV DB_PASSWORD=secret 13 | ENV VWSFRIEND_USERNAME=admin 14 | ENV VWSFRIEND_PASSWORD=secret 15 | ENV VWSFRIEND_HOSTNAME= 16 | ENV VWSFRIEND_PORT= 17 | ENV DB_HOSTNAME=postgresdbbackend 18 | ENV DB_PORT=5432 19 | ENV DB_NAME=vwsfriend 20 | ENV GF_EXPLORE_ENABLED=false 21 | ENV GF_ALERTING_ENABLED=false 22 | ENV GF_METRICS_ENABLED=false 23 | ENV GF_EXPRESSIONS_ENABLED=false 24 | ENV GF_PLUGINS_PLUGIN_ADMIN_ENABLED=false 25 | ENV GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH="/var/lib/grafana-static/dashboards/vwsfriend/VWsFriend/overview.json" 26 | ENV GF_FEATURE_TOGGLES_ENABLE="newPanelChromeUI,topNavCommandPalette" 27 | 28 | COPY ./config/grafana/provisioning/ /etc/grafana/provisioning/ 29 | COPY ./dashboards/ /var/lib/grafana-static/dashboards/ 30 | COPY ./public/img/ /usr/share/grafana/public/img/ 31 | 32 | EXPOSE 3000 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/charger.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Float, Integer, ForeignKey, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | 6 | 7 | class Charger(Base): 8 | __tablename__ = 'chargers' 9 | id = Column(String, primary_key=True) 10 | name = Column(String) 11 | latitude = Column(Float) 12 | longitude = Column(Float) 13 | address = Column(String) 14 | max_power = Column(Float) 15 | num_spots = Column(Integer) 16 | operator_id = Column(String, ForeignKey('operators.id')) 17 | operator = relationship("Operator") 18 | custom = Column(Boolean) 19 | 20 | def __init__(self, id, custom=False): 21 | self.id = id 22 | self.custom = custom 23 | 24 | 25 | class Operator(Base): 26 | __tablename__ = 'operators' 27 | id = Column(String, primary_key=True) 28 | name = Column(String) 29 | phone = Column(String) 30 | custom = Column(Boolean) 31 | 32 | def __init__(self, id, name, phone, custom=False): 33 | self.id = id 34 | self.name = name 35 | self.phone = phone 36 | self.custom = custom 37 | 38 | def displayString(self): 39 | return self.name 40 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/6c4cdc5006ba_added_battery_temperature_table.py: -------------------------------------------------------------------------------- 1 | """Added battery temperature table 2 | 3 | Revision ID: 6c4cdc5006ba 4 | Revises: f3a66cd08ebe 5 | Create Date: 2023-10-27 12:12:09.450271 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '6c4cdc5006ba' 15 | down_revision = 'f3a66cd08ebe' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.create_table('battery_temperature', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('vehicle_vin', sa.String(), nullable=True), 24 | sa.Column('carCapturedTimestamp', DatetimeDecorator(timezone=True), nullable=False), 25 | sa.Column('temperatureHvBatteryMin_K', sa.Float(), nullable=True), 26 | sa.Column('temperatureHvBatteryMax_K', sa.Float(), nullable=True), 27 | sa.ForeignKeyConstraint(['vehicle_vin'], ['vehicles.vin'], ), 28 | sa.PrimaryKeyConstraint('id'), 29 | sa.UniqueConstraint('vehicle_vin', 'carCapturedTimestamp') 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table('battery_temperature') 35 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/06a5ec0a1af0_add_settings.py: -------------------------------------------------------------------------------- 1 | """add settings 2 | 3 | Revision ID: 06a5ec0a1af0 4 | Revises: 90636c798967 5 | Create Date: 2021-09-08 08:38:26.189013 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '06a5ec0a1af0' 14 | down_revision = '90636c798967' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('settings', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('unit_of_length', sa.Enum('KM', 'MI', name='unitoflength'), nullable=True), 24 | sa.Column('unit_of_temperature', sa.Enum('C', 'F', name='unitoftemperature'), nullable=True), 25 | sa.Column('grafana_url', sa.String(), nullable=True), 26 | sa.Column('vwsfriend_url', sa.String(), nullable=True), 27 | sa.Column('locale', sa.String(), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('settings') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/027f3fc22787_add_invalid_climatization_state.py: -------------------------------------------------------------------------------- 1 | """Add invalid climatization state 2 | 3 | Revision ID: 027f3fc22787 4 | Revises: 79ac0505ad35 5 | Create Date: 2022-03-15 08:25:02.114964 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '027f3fc22787' 14 | down_revision = '79ac0505ad35' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE climatizationstate ADD VALUE IF NOT EXISTS 'INVALID'") 23 | 24 | 25 | def downgrade(): 26 | if op.get_context().dialect.name == 'postgresql': 27 | op.execute("ALTER TYPE climatizationstate RENAME TO climatizationstate_old") 28 | op.execute("CREATE TYPE climatizationstate AS ENUM('UNKNOWN', 'VENTILATION', 'COOLING', 'HEATING', 'OFF')") 29 | op.execute(( 30 | "ALTER TABLE transactions ALTER COLUMN climatizationstate TYPE climatizationstate USING " 31 | "climatizationstate::text::climatizationstate" 32 | )) 33 | op.execute("DROP TYPE climatizationstate_old") 34 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/climatization.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Enum, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | from weconnect.elements.climatization_status import ClimatizationStatus 8 | 9 | 10 | class Climatization(Base): 11 | __tablename__ = 'climatization' 12 | __table_args__ = ( 13 | UniqueConstraint('vehicle_vin', 'carCapturedTimestamp'), 14 | ) 15 | id = Column(Integer, primary_key=True) 16 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 17 | carCapturedTimestamp = Column(DatetimeDecorator(timezone=True), nullable=False) 18 | vehicle = relationship("Vehicle") 19 | remainingClimatisationTime_min = Column(Integer) 20 | climatisationState = Column(Enum(ClimatizationStatus.ClimatizationState)) 21 | 22 | def __init__(self, vehicle, carCapturedTimestamp, remainingClimatisationTime_min, climatisationState): 23 | self.vehicle = vehicle 24 | self.carCapturedTimestamp = carCapturedTimestamp 25 | self.remainingClimatisationTime_min = remainingClimatisationTime_min 26 | self.climatisationState = climatisationState 27 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/refuel_session_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}All charging sessions in database{% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for session in sessions %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 |
    IDDateStart %End %StationVehicle
    {{ session.id }}{{ session.date }}{{ session.startSOC_pct }}{{ session.endSOC_pct }}{% if session.location %}{{ session.location.displayString() }}{% endif %}{{ session.vehicle.displayString() }}
    28 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/charging_session_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}All charging sessions in database{% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for session in sessions %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 |
    IDStart DateStart SoCEnd SoCChargerVehicle
    {{ session.id }}{{ session.started }}{{ session.startSOC_pct }}{{ session.endSOC_pct }}{% if session.charger %}{{ session.charger.name }}{% endif %}{{ session.vehicle.displayString() }}
    28 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/91dac376210b_new_enum_values.py: -------------------------------------------------------------------------------- 1 | """New enum values 2 | 3 | Revision ID: 91dac376210b 4 | Revises: f6785099280a 5 | Create Date: 2021-12-03 09:02:25.107132 6 | 7 | """ 8 | from alembic import op 9 | 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '91dac376210b' 13 | down_revision = 'f6785099280a' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | if op.get_context().dialect.name == 'postgresql': 20 | with op.get_context().autocommit_block(): 21 | op.execute("ALTER TYPE chargingstate ADD VALUE IF NOT EXISTS 'CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING'") 22 | op.execute("ALTER TYPE chargingstate ADD VALUE IF NOT EXISTS 'CHARGE_PURPOSE_REACHED_CONSERVATION'") 23 | 24 | 25 | def downgrade(): 26 | if op.get_context().dialect.name == 'postgresql': 27 | op.execute("ALTER TYPE chargingstate RENAME TO chargingstate_old") 28 | op.execute("CREATE TYPE chargingstate AS ENUM('ERROR', 'CHARGING', 'READY_FOR_CHARGING', 'OFF', 'UNKNOWN')") 29 | op.execute(( 30 | "ALTER TABLE transactions ALTER COLUMN chargingstate TYPE chargingstate USING " 31 | "chargingstate::text::chargingstate" 32 | )) 33 | op.execute("DROP TYPE chargingstate_old") 34 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/settings/homekit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}Homekit Settings {% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    To connect your Home to the cars managed by VWsFriend please scan the following QR Code with the Home App on your smartphone.

    9 | 10 |

    You can also manually add the Bridge {{ current_app.homekitDriver.accessory.display_name }} with the Passcode: {{ current_app.homekitDriver.state.pincode.decode('utf8') }}

    11 |

    Status:

    12 |

    13 |

    14 | {{ form.csrf_token }} 15 | The bridge is currently {{ 'paired' if current_app.homekitDriver.state.paired else 'unpaired' }}{{ form.unpair(disabled=(not current_app.homekitDriver.state.paired)) }} 16 |
    17 |

    18 |

    Available Homekit Accessories:

    19 |

    20 |

      21 | {% for accessory in current_app.homekitDriver.accessory.accessories.values() %} 22 |
    • {{ accessory.display_name }} (AID: {{ accessory.aid }}) Services: {% for service in accessory.services %}{{service.display_name}}, {% endfor %}
    • 23 | {% endfor %} 24 |
    25 |

    26 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/184bb7a71ada_added_error_table.py: -------------------------------------------------------------------------------- 1 | """Added error table 2 | 3 | Revision ID: 184bb7a71ada 4 | Revises: 06a5ec0a1af0 5 | Create Date: 2021-09-28 11:06:02.808351 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = '184bb7a71ada' 16 | down_revision = '06a5ec0a1af0' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('weconnect_errors', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('datetime', DatetimeDecorator(timezone=True), nullable=False), 26 | sa.Column('errortype', sa.Enum('HTTP', 'TIMEOUT', 'CONNECTION', 'ALL', name='erroreventtype'), nullable=True), 27 | sa.Column('detail', sa.String(), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('weconnect_errors') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/a2ba1b877ee7_add_meta_information_for_charging_.py: -------------------------------------------------------------------------------- 1 | """Add meta information for charging session 2 | 3 | Revision ID: a2ba1b877ee7 4 | Revises: d13dfd852ab8 5 | Create Date: 2023-02-20 10:36:21.132908 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a2ba1b877ee7' 14 | down_revision = 'd13dfd852ab8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('charging_sessions', sa.Column('meterStart_kWh', sa.Float(), nullable=True)) 21 | op.add_column('charging_sessions', sa.Column('meterEnd_kWh', sa.Float(), nullable=True)) 22 | op.add_column('charging_sessions', sa.Column('pricePerKwh_ct', sa.Float(), nullable=True)) 23 | op.add_column('charging_sessions', sa.Column('pricePerMinute_ct', sa.Float(), nullable=True)) 24 | op.add_column('charging_sessions', sa.Column('pricePerSession_ct', sa.Float(), nullable=True)) 25 | 26 | 27 | def downgrade(): 28 | op.drop_column('charging_sessions', 'pricePerSession_ct') 29 | op.drop_column('charging_sessions', 'pricePerMinute_ct') 30 | op.drop_column('charging_sessions', 'pricePerKwh_ct') 31 | op.drop_column('charging_sessions', 'meterEnd_kWh') 32 | op.drop_column('charging_sessions', 'meterStart_kWh') 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f917fc564c1d_new_cartype.py: -------------------------------------------------------------------------------- 1 | """New cartype 2 | 3 | Revision ID: f917fc564c1d 4 | Revises: 9a21960991c1 5 | Create Date: 2022-08-02 08:13:48.359712 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f917fc564c1d' 14 | down_revision = '9a21960991c1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE cartype ADD VALUE IF NOT EXISTS 'PETROL '") 23 | op.execute("ALTER TYPE cartype ADD VALUE IF NOT EXISTS 'DIESEL'") 24 | op.execute("ALTER TYPE cartype ADD VALUE IF NOT EXISTS 'CNG'") 25 | op.execute("ALTER TYPE cartype ADD VALUE IF NOT EXISTS 'LPG'") 26 | 27 | 28 | def downgrade(): 29 | if op.get_context().dialect.name == 'postgresql': 30 | op.execute("ALTER TYPE cartype RENAME TO cartype_old") 31 | op.execute("CREATE TYPE cartype AS ENUM('ELECTRIC', 'HYBRID', 'GASOLINE', 'UNKNOWN')") 32 | op.execute(( 33 | "ALTER TABLE transactions ALTER COLUMN cartype TYPE cartype USING " 34 | "cartype::text::cartype" 35 | )) 36 | op.execute("DROP TYPE cartype_old") 37 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/journey.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey, Table, UniqueConstraint 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | journey_tag_association_table = Table('journey_tag', Base.metadata, 9 | Column('journey_id', ForeignKey('journey.id')), 10 | Column('tag_name', ForeignKey('tag.name')) 11 | ) 12 | 13 | 14 | class Journey(Base): 15 | __tablename__ = 'journey' 16 | __table_args__ = ( 17 | UniqueConstraint('vehicle_vin', 'start'), 18 | ) 19 | id = Column(Integer, primary_key=True) 20 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 21 | start = Column(DatetimeDecorator(timezone=True), nullable=False) 22 | end = Column(DatetimeDecorator(timezone=True), nullable=False) 23 | vehicle = relationship("Vehicle") 24 | title = Column(String) 25 | description = Column(String) 26 | tags = relationship("Tag", secondary=journey_tag_association_table, backref=backref("journey")) 27 | 28 | def __init__(self, vehicle, start, end, title): 29 | self.vehicle = vehicle 30 | self.start = start 31 | self.end = end 32 | self.title = title 33 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/warning_light.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Boolean, Enum, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from weconnect.elements.warning_lights_status import WarningLightsStatus 5 | 6 | from vwsfriend.model.base import Base 7 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 8 | 9 | 10 | class WarningLight(Base): 11 | __tablename__ = 'warning_lights' 12 | __table_args__ = ( 13 | UniqueConstraint('vehicle_vin', 'messageId', 'start'), 14 | ) 15 | id = Column(Integer, primary_key=True) 16 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 17 | start = Column(DatetimeDecorator(timezone=True), nullable=False) 18 | start_mileage = Column(Integer) 19 | end = Column(DatetimeDecorator(timezone=True)) 20 | end_mileage = Column(Integer) 21 | vehicle = relationship("Vehicle") 22 | text = Column(String) 23 | category = Column(Enum(WarningLightsStatus.WarningLight.Category)) 24 | messageId = Column(String) 25 | priority = Column(Integer) 26 | serviceLead = Column(Boolean) 27 | customerRelevance = Column(Boolean) 28 | 29 | def __init__(self, vehicle, messageId, start, text, category): 30 | self.vehicle = vehicle 31 | self.messageId = messageId 32 | self.start = start 33 | self.text = text 34 | self.category = category 35 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/689f13ef5c86_added_responsetime_table.py: -------------------------------------------------------------------------------- 1 | """Added responsetime table 2 | 3 | Revision ID: 689f13ef5c86 4 | Revises: 023aa9f36740 5 | Create Date: 2021-10-11 21:09:14.774041 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = '689f13ef5c86' 16 | down_revision = '023aa9f36740' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('weconnect_responsetime', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('datetime', DatetimeDecorator(timezone=True), nullable=False), 26 | sa.Column('min', sa.Float(), nullable=True), 27 | sa.Column('avg', sa.Float(), nullable=True), 28 | sa.Column('max', sa.Float(), nullable=True), 29 | sa.Column('total', sa.Float(), nullable=True), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade(): 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_table('weconnect_responsetime') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/9a21960991c1_new_enum_values.py: -------------------------------------------------------------------------------- 1 | """New enum values 2 | 3 | Revision ID: 9a21960991c1 4 | Revises: b117e50b5aa4 5 | Create Date: 2022-06-28 16:17:02.825165 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9a21960991c1' 14 | down_revision = 'b117e50b5aa4' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if op.get_context().dialect.name == 'postgresql': 21 | with op.get_context().autocommit_block(): 22 | op.execute("ALTER TYPE chargingstate ADD VALUE IF NOT EXISTS 'DISCHARGING'") 23 | op.execute("ALTER TYPE chargingstate ADD VALUE IF NOT EXISTS 'UNSUPPORTED'") 24 | 25 | 26 | def downgrade(): 27 | if op.get_context().dialect.name == 'postgresql': 28 | op.execute("ALTER TYPE chargingstate RENAME TO chargingstate_old") 29 | op.execute("CREATE TYPE chargingstate AS ENUM('ERROR', 'CHARGING', 'READY_FOR_CHARGING', 'OFF', 'UNKNOWN', 'NOT_READY_FOR_CHARGING', 'CONSERVATION', 'CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING', 'CHARGE_PURPOSE_REACHED_CONSERVATION')") 30 | op.execute(( 31 | "ALTER TABLE transactions ALTER COLUMN chargingstate TYPE chargingstate USING " 32 | "chargingstate::text::chargingstate" 33 | )) 34 | op.execute("DROP TYPE chargingstate_old") 35 | 36 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/operator_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block header %} 7 |

    {% block title %}{% if form and form.id.data %}Edit settings for {% else %}Add a new {% endif %}operator{% endblock %}

    8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if form %} 12 |
    13 |
    14 | {{ form.csrf_token }} 15 | 16 | {{ form.id }} 17 | 18 |
    19 | {{ form.name.label }} 20 | {{ form.name }} 21 | {% if form.name.errors %} 22 |
      23 | {% for error in form.name.errors %} 24 |
    • {{ error }}
    • 25 | {% endfor %} 26 |
    27 | {% endif %} 28 |
    29 | 30 |
    31 | {{ form.phone.label }} 32 | {{ form.phone }} 33 | {% if form.phone.errors %} 34 |
      35 | {% for error in form.phone.errors %} 36 |
    • {{ error }}
    • 37 | {% endfor %} 38 |
    39 | {% endif %} 40 |
    41 | 42 | {% if form.id.data %} 43 | {{ form.save }} 44 | {% else %} 45 | {{ form.add }} 46 | {% endif %} 47 | {{ form.delete }} 48 | 49 |
    50 |
    51 | {% endif %} 52 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/maintenance.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from sqlalchemy import Column, Integer, String, Enum, ForeignKey 3 | from sqlalchemy.orm import relationship 4 | 5 | from vwsfriend.model.base import Base 6 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 7 | 8 | 9 | class MaintenanceType(enum.Enum,): 10 | INSPECTION = enum.auto() 11 | OIL_SERVICE = enum.auto() 12 | UNKNOWN = enum.auto() 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | @classmethod 18 | def choices(cls): 19 | return [(choice, choice.name) for choice in cls] 20 | 21 | @classmethod 22 | def coerce(cls, item): 23 | return item if isinstance(item, MaintenanceType) else MaintenanceType[item] 24 | 25 | 26 | class Maintenance(Base): 27 | __tablename__ = 'maintenance' 28 | id = Column(Integer, primary_key=True) 29 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 30 | vehicle = relationship("Vehicle") 31 | date = Column(DatetimeDecorator(timezone=True)) 32 | mileage = Column(Integer) 33 | type = Column(Enum(MaintenanceType)) 34 | due_in_days = Column(Integer) 35 | due_in_km = Column(Integer) 36 | 37 | def __init__(self, vehicle, date, mileage, type, due_in_days, due_in_km): 38 | self.vehicle = vehicle 39 | self.date = date 40 | self.mileage = mileage 41 | self.type = type 42 | self.due_in_days = due_in_days 43 | self.due_in_km = due_in_km 44 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/range.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | class Range(Base): 9 | __tablename__ = 'ranges' 10 | __table_args__ = ( 11 | UniqueConstraint('vehicle_vin', 'carCapturedTimestamp'), 12 | ) 13 | id = Column(Integer, primary_key=True) 14 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 15 | carCapturedTimestamp = Column(DatetimeDecorator(timezone=True), nullable=False) 16 | vehicle = relationship("Vehicle") 17 | totalRange_km = Column(Integer) 18 | primary_currentSOC_pct = Column(Integer) 19 | primary_remainingRange_km = Column(Integer) 20 | secondary_currentSOC_pct = Column(Integer) 21 | secondary_remainingRange_km = Column(Integer) 22 | 23 | def __init__(self, vehicle, carCapturedTimestamp, totalRange_km, primary_currentSOC_pct, primary_remainingRange_km, secondary_currentSOC_pct, 24 | secondary_remainingRange_km): 25 | self.vehicle = vehicle 26 | self.carCapturedTimestamp = carCapturedTimestamp 27 | self.totalRange_km = totalRange_km 28 | self.primary_currentSOC_pct = primary_currentSOC_pct 29 | self.primary_remainingRange_km = primary_remainingRange_km 30 | self.secondary_currentSOC_pct = secondary_currentSOC_pct 31 | self.secondary_remainingRange_km = secondary_remainingRange_km 32 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/f2fc2726507f_added_journey_table.py: -------------------------------------------------------------------------------- 1 | """Added journey table 2 | 3 | Revision ID: f2fc2726507f 4 | Revises: 689f13ef5c86 5 | Create Date: 2021-10-11 21:23:19.926318 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'f2fc2726507f' 15 | down_revision = '689f13ef5c86' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('journey', 23 | sa.Column('id', sa.Integer(), nullable=False), 24 | sa.Column('vehicle_vin', sa.String(), nullable=True), 25 | sa.Column('start', DatetimeDecorator(timezone=True), nullable=False), 26 | sa.Column('end', DatetimeDecorator(timezone=True), nullable=False), 27 | sa.Column('title', sa.String(), nullable=True), 28 | sa.Column('description', sa.String(), nullable=True), 29 | sa.ForeignKeyConstraint(['vehicle_vin'], ['vehicles.vin'], ), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('vehicle_vin', 'start') 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table('journey') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/3500b84eb1f1_create_maintenance_table_and_add_.py: -------------------------------------------------------------------------------- 1 | """create maintenance table and add priority to warning lights table 2 | 3 | Revision ID: 3500b84eb1f1 4 | Revises: f69cf5eca1e2 5 | Create Date: 2022-04-12 12:16:04.988381 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '3500b84eb1f1' 15 | down_revision = 'f69cf5eca1e2' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.create_table('maintenance', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('vehicle_vin', sa.String(), nullable=True), 24 | sa.Column('date', DatetimeDecorator(timezone=True), nullable=True), 25 | sa.Column('mileage', sa.Integer(), nullable=True), 26 | sa.Column('type', sa.Enum('INSPECTION', 'OIL_SERVICE', 'UNKNOWN', name='maintenancetype'), nullable=True), 27 | sa.Column('due_in_days', sa.Integer(), nullable=True), 28 | sa.Column('due_in_km', sa.Integer(), nullable=True), 29 | sa.ForeignKeyConstraint(['vehicle_vin'], ['vehicles.vin'], ), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | op.add_column('warning_lights', sa.Column('priority', sa.Integer(), nullable=True)) 33 | 34 | 35 | def downgrade(): 36 | op.drop_column('warning_lights', 'priority') 37 | op.drop_table('maintenance') 38 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/login/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}Login {% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 | {{ form.csrf_token }} 11 | 12 |
    13 | {{ form.username.label }} 14 | {{ form.username(size=20) }} 15 | {% if form.username.errors %} 16 |
      17 | {% for error in form.username.errors %} 18 |
    • {{ error }}
    • 19 | {% endfor %} 20 |
    21 | {% endif %} 22 |
    23 |
    24 | {{ form.password.label }} 25 | {{ form.password(size=20) }} 26 | {% if form.password.errors %} 27 |
      28 | {% for error in form.password.errors %} 29 |
    • {{ error }}
    • 30 | {% endfor %} 31 |
    32 | {% endif %} 33 |
    34 |
    35 | {{ form.remember_me.label }} 36 | {{ form.remember_me }} 37 | {% if form.remember_me.errors %} 38 |
      39 | {% for error in form.remember_me.errors %} 40 |
    • {{ error }}
    • 41 | {% endfor %} 42 |
    43 | {% endif %} 44 |
    45 | {{ form.submit }} 46 | 47 |
    48 |
    49 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_warning_light/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthWarnings": { 13 | "warningLights": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(-300)", 16 | "mileage_km": 12458, 17 | "warningLights": 18 | [] 19 | } 20 | } 21 | } 22 | }, 23 | "now(+0)" 24 | ], 25 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 26 | { 27 | "data": [ 28 | { 29 | "vin": "AAABBBCCCDDD12345", 30 | "role": "PRIMARY_USER", 31 | "enrollmentStatus": "COMPLETED", 32 | "model": "Demo Test Vehicle", 33 | "nickname": "Test Vehicle", 34 | "capabilities": [{ 35 | "id": "parkingPosition", 36 | "expirationDate": "2051-05-12T11:53:00Z", 37 | "userDisablingAllowed": false 38 | }], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/bd42a6f71742_added_warning_light_table.py: -------------------------------------------------------------------------------- 1 | """Added account table 2 | 3 | Revision ID: bd42a6f71742 4 | Revises: 027f3fc22787 5 | Create Date: 2022-04-08 21:33:03.935283 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'bd42a6f71742' 15 | down_revision = '027f3fc22787' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.create_table('warning_lights', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('vehicle_vin', sa.String(), nullable=True), 24 | sa.Column('start', DatetimeDecorator(timezone=True), nullable=False), 25 | sa.Column('end', DatetimeDecorator(timezone=True), nullable=True), 26 | sa.Column('text', sa.String(), nullable=True), 27 | sa.Column('category', sa.Enum('LIGHTING', 'UNKNOWN', name='category'), nullable=True), 28 | sa.Column('messageId', sa.String(), nullable=True), 29 | sa.Column('serviceLead', sa.Boolean(), nullable=True), 30 | sa.Column('customerRelevance', sa.Boolean(), nullable=True), 31 | sa.ForeignKeyConstraint(['vehicle_vin'], ['vehicles.vin'], ), 32 | sa.PrimaryKeyConstraint('id'), 33 | sa.UniqueConstraint('vehicle_vin', 'messageId', 'start') 34 | ) 35 | 36 | 37 | def downgrade(): 38 | op.drop_table('warning_lights') 39 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_warning_light/004_0s_light_off.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthWarnings": { 13 | "warningLights": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(+300)", 16 | "mileage_km": 12500, 17 | "warningLights": 18 | [] 19 | } 20 | } 21 | } 22 | }, 23 | "now(+0)" 24 | ], 25 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 26 | { 27 | "data": [ 28 | { 29 | "vin": "AAABBBCCCDDD12345", 30 | "role": "PRIMARY_USER", 31 | "enrollmentStatus": "COMPLETED", 32 | "model": "Demo Test Vehicle", 33 | "nickname": "Test Vehicle", 34 | "capabilities": [{ 35 | "id": "parkingPosition", 36 | "expirationDate": "2051-05-12T11:53:00Z", 37 | "userDisablingAllowed": false 38 | }], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/charge.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, Float, String, Enum, ForeignKey, UniqueConstraint, Index 2 | from sqlalchemy.orm import relationship 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | from weconnect.elements.charging_status import ChargingStatus 8 | 9 | 10 | class Charge(Base): 11 | __tablename__ = 'charges' 12 | __table_args__ = ( 13 | UniqueConstraint('vehicle_vin', 'carCapturedTimestamp'), 14 | Index("charges_idx_vehicle_vin_chargepower_kw", "vehicle_vin", "chargePower_kW"), 15 | ) 16 | id = Column(Integer, primary_key=True) 17 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 18 | carCapturedTimestamp = Column(DatetimeDecorator(timezone=True), nullable=False) 19 | vehicle = relationship("Vehicle") 20 | remainingChargingTimeToComplete_min = Column(Integer) 21 | chargingState = Column(Enum(ChargingStatus.ChargingState), index=True) 22 | chargeMode = Column(Enum(ChargingStatus.ChargeMode)) 23 | chargePower_kW = Column(Float, index=True) 24 | chargeRate_kmph = Column(Float) 25 | 26 | def __init__(self, vehicle, carCapturedTimestamp, remainingChargingTimeToComplete_min, chargingState, chargeMode, chargePower_kW, chargeRate_kmph): 27 | self.vehicle = vehicle 28 | self.carCapturedTimestamp = carCapturedTimestamp 29 | self.remainingChargingTimeToComplete_min = remainingChargingTimeToComplete_min 30 | self.chargingState = chargingState 31 | self.chargeMode = chargeMode 32 | self.chargePower_kW = chargePower_kW 33 | self.chargeRate_kmph = chargeRate_kmph 34 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/static/style.css: -------------------------------------------------------------------------------- 1 | html { font-family: sans-serif; background: #eee; padding: 1rem;} 2 | body { max-width: 960px; margin: 0 auto; background: white;} 3 | h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } 4 | h2 { font-family: serif; color: #377ba8; margin: 1rem 0; } 5 | a { color: #377ba8; } 6 | 7 | a.button { 8 | -webkit-appearance: button; 9 | -moz-appearance: button; 10 | appearance: button; 11 | 12 | text-decoration: none; 13 | color: initial; 14 | } 15 | 16 | hr { border: none; border-top: 1px solid lightgray; } 17 | nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } 18 | nav h1 { flex: auto; margin: 0; } 19 | nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } 20 | nav ul { display: flex; list-style: none; margin: 0; padding: 0; } 21 | nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } 22 | .content { padding: 0 1rem 1rem;} 23 | .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } 24 | .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } 25 | .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } 26 | input.danger { color: #cc2f2e; } 27 | input[type=submit] { align-self: start; min-width: 10em; } 28 | 29 | #footer { 30 | position: fixed; 31 | left: 0px; 32 | bottom: 0px; 33 | height: 1em; 34 | width: 100%; 35 | text-align: center; 36 | background: #eee; 37 | padding: 0.25rem 38 | } 39 | 40 | img.status { 41 | width: 388px; 42 | height: auto; 43 | } 44 | 45 | img.icon { 46 | width: 30px; 47 | height: auto; 48 | } 49 | 50 | #mapid { height: 300px; } 51 | 52 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/id_trip/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "measurements": { 5 | "odometerStatus": { 6 | "value": { 7 | "carCapturedTimestamp": "demodate(-300)", 8 | "odometer": 4166 9 | } 10 | } 11 | }, 12 | "readiness": { 13 | "readinessStatus": { 14 | "connectionState": { 15 | "isOnline": true, 16 | "isActive": false, 17 | "batteryPowerLevel": "comfort", 18 | "dailyPowerBudgetAvailable": true 19 | }, 20 | "connectionWarning": { 21 | "insufficientBatteryLevelWarning": false, 22 | "dailyPowerBudgetWarning": false 23 | } 24 | } 25 | } 26 | }, 27 | "now(+0)" 28 | ], 29 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 30 | { 31 | "data": [ 32 | { 33 | "vin": "AAABBBCCCDDD12345", 34 | "role": "PRIMARY_USER", 35 | "enrollmentStatus": "COMPLETED", 36 | "model": "Demo Test Vehicle", 37 | "nickname": "Test Vehicle", 38 | "capabilities": [], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/id_trip/002_0s_parking.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "measurements": { 5 | "odometerStatus": { 6 | "value": { 7 | "carCapturedTimestamp": "demodate(300)", 8 | "odometer": 4166 9 | } 10 | } 11 | }, 12 | "readiness": { 13 | "readinessStatus": { 14 | "connectionState": { 15 | "isOnline": true, 16 | "isActive": false, 17 | "batteryPowerLevel": "comfort", 18 | "dailyPowerBudgetAvailable": true 19 | }, 20 | "connectionWarning": { 21 | "insufficientBatteryLevelWarning": false, 22 | "dailyPowerBudgetWarning": false 23 | } 24 | } 25 | } 26 | }, 27 | "now(+0)" 28 | ], 29 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 30 | { 31 | "data": [ 32 | { 33 | "vin": "AAABBBCCCDDD12345", 34 | "role": "PRIMARY_USER", 35 | "enrollmentStatus": "COMPLETED", 36 | "model": "Demo Test Vehicle", 37 | "nickname": "Test Vehicle", 38 | "capabilities": [], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/id_trip/003_0s_driving.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "measurements": { 5 | "odometerStatus": { 6 | "value": { 7 | "carCapturedTimestamp": "demodate(600)", 8 | "odometer": 4166 9 | } 10 | } 11 | }, 12 | "readiness": { 13 | "readinessStatus": { 14 | "connectionState": { 15 | "isOnline": true, 16 | "isActive": true, 17 | "batteryPowerLevel": "comfort", 18 | "dailyPowerBudgetAvailable": true 19 | }, 20 | "connectionWarning": { 21 | "insufficientBatteryLevelWarning": false, 22 | "dailyPowerBudgetWarning": false 23 | } 24 | } 25 | } 26 | }, 27 | "now(+0)" 28 | ], 29 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 30 | { 31 | "data": [ 32 | { 33 | "vin": "AAABBBCCCDDD12345", 34 | "role": "PRIMARY_USER", 35 | "enrollmentStatus": "COMPLETED", 36 | "model": "Demo Test Vehicle", 37 | "nickname": "Test Vehicle", 38 | "capabilities": [], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/id_trip/004_0s_parking2.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "measurements": { 5 | "odometerStatus": { 6 | "value": { 7 | "carCapturedTimestamp": "demodate(800)", 8 | "odometer": 4180 9 | } 10 | } 11 | }, 12 | "readiness": { 13 | "readinessStatus": { 14 | "connectionState": { 15 | "isOnline": true, 16 | "isActive": false, 17 | "batteryPowerLevel": "comfort", 18 | "dailyPowerBudgetAvailable": true 19 | }, 20 | "connectionWarning": { 21 | "insufficientBatteryLevelWarning": false, 22 | "dailyPowerBudgetWarning": false 23 | } 24 | } 25 | } 26 | }, 27 | "now(+0)" 28 | ], 29 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 30 | { 31 | "data": [ 32 | { 33 | "vin": "AAABBBCCCDDD12345", 34 | "role": "PRIMARY_USER", 35 | "enrollmentStatus": "COMPLETED", 36 | "model": "Demo Test Vehicle", 37 | "nickname": "Test Vehicle", 38 | "capabilities": [], 39 | "images": {}, 40 | "coUsers": [] 41 | } 42 | ] 43 | }, 44 | "now(+0)" 45 | ] 46 | } -------------------------------------------------------------------------------- /grafana/public/img/icons/gas.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_maintenance/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthInspection": { 13 | "maintenanceStatus": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(-300)", 16 | "inspectionDue_days": 10, 17 | "inspectionDue_km": 20, 18 | "mileage_km": 50, 19 | "oilServiceDue_days": 30, 20 | "oilServiceDue_km": 40 21 | } 22 | } 23 | } 24 | }, 25 | "now(+0)" 26 | ], 27 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 28 | { 29 | "data": [ 30 | { 31 | "vin": "AAABBBCCCDDD12345", 32 | "role": "PRIMARY_USER", 33 | "enrollmentStatus": "COMPLETED", 34 | "model": "Demo Test Vehicle", 35 | "nickname": "Test Vehicle", 36 | "capabilities": [{ 37 | "id": "parkingPosition", 38 | "expirationDate": "2051-05-12T11:53:00Z", 39 | "userDisablingAllowed": false 40 | }], 41 | "images": {}, 42 | "coUsers": [] 43 | } 44 | ] 45 | }, 46 | "now(+0)" 47 | ] 48 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_maintenance/002_0s_drive.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthInspection": { 13 | "maintenanceStatus": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(00)", 16 | "inspectionDue_days": 9, 17 | "inspectionDue_km": 18, 18 | "mileage_km": 60, 19 | "oilServiceDue_days": 29, 20 | "oilServiceDue_km": 38 21 | } 22 | } 23 | } 24 | }, 25 | "now(+0)" 26 | ], 27 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 28 | { 29 | "data": [ 30 | { 31 | "vin": "AAABBBCCCDDD12345", 32 | "role": "PRIMARY_USER", 33 | "enrollmentStatus": "COMPLETED", 34 | "model": "Demo Test Vehicle", 35 | "nickname": "Test Vehicle", 36 | "capabilities": [{ 37 | "id": "parkingPosition", 38 | "expirationDate": "2051-05-12T11:53:00Z", 39 | "userDisablingAllowed": false 40 | }], 41 | "images": {}, 42 | "coUsers": [] 43 | } 44 | ] 45 | }, 46 | "now(+0)" 47 | ] 48 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_maintenance/004_0s_drive2.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthInspection": { 13 | "maintenanceStatus": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(600)", 16 | "inspectionDue_days": 99, 17 | "inspectionDue_km": 990, 18 | "mileage_km": 160, 19 | "oilServiceDue_days": 49, 20 | "oilServiceDue_km": 490 21 | } 22 | } 23 | } 24 | }, 25 | "now(+0)" 26 | ], 27 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 28 | { 29 | "data": [ 30 | { 31 | "vin": "AAABBBCCCDDD12345", 32 | "role": "PRIMARY_USER", 33 | "enrollmentStatus": "COMPLETED", 34 | "model": "Demo Test Vehicle", 35 | "nickname": "Test Vehicle", 36 | "capabilities": [{ 37 | "id": "parkingPosition", 38 | "expirationDate": "2051-05-12T11:53:00Z", 39 | "userDisablingAllowed": false 40 | }], 41 | "images": {}, 42 | "coUsers": [] 43 | } 44 | ] 45 | }, 46 | "now(+0)" 47 | ] 48 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_maintenance/003_0s_all_maintenance.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "vehicleHealthInspection": { 13 | "maintenanceStatus": { 14 | "value": { 15 | "carCapturedTimestamp": "demodate(300)", 16 | "inspectionDue_days": 100, 17 | "inspectionDue_km": 1000, 18 | "mileage_km": 60, 19 | "oilServiceDue_days": 50, 20 | "oilServiceDue_km": 500 21 | } 22 | } 23 | } 24 | }, 25 | "now(+0)" 26 | ], 27 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 28 | { 29 | "data": [ 30 | { 31 | "vin": "AAABBBCCCDDD12345", 32 | "role": "PRIMARY_USER", 33 | "enrollmentStatus": "COMPLETED", 34 | "model": "Demo Test Vehicle", 35 | "nickname": "Test Vehicle", 36 | "capabilities": [{ 37 | "id": "parkingPosition", 38 | "expirationDate": "2051-05-12T11:53:00Z", 39 | "userDisablingAllowed": false 40 | }], 41 | "images": {}, 42 | "coUsers": [] 43 | } 44 | ] 45 | }, 46 | "now(+0)" 47 | ] 48 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/901e3e466d03_add_changes_to_allow_geofencing_and_.py: -------------------------------------------------------------------------------- 1 | """Add changes to allow geofencing and custom chargers / operators 2 | 3 | Revision ID: 901e3e466d03 4 | Revises: eb4c7c65c4fb 5 | Create Date: 2022-02-07 08:46:25.177834 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '901e3e466d03' 14 | down_revision = 'eb4c7c65c4fb' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('geofences', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('name', sa.String(), nullable=True), 23 | sa.Column('latitude', sa.Float(), nullable=True), 24 | sa.Column('longitude', sa.Float(), nullable=True), 25 | sa.Column('radius', sa.Float(), nullable=True), 26 | sa.Column('location_id', sa.BigInteger(), nullable=True), 27 | sa.Column('charger_id', sa.String(), nullable=True), 28 | sa.ForeignKeyConstraint(['charger_id'], ['chargers.id'], ), 29 | sa.ForeignKeyConstraint(['location_id'], ['locations.osm_id'], ), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | op.add_column('chargers', sa.Column('custom', sa.Boolean(), nullable=True)) 33 | op.execute("UPDATE chargers SET custom = false") 34 | op.add_column('operators', sa.Column('custom', sa.Boolean(), nullable=True)) 35 | op.execute("UPDATE operators SET custom = false") 36 | 37 | if op.get_context().dialect.name == 'postgresql': 38 | op.execute(sa.text('CREATE extension IF NOT EXISTS cube;')) 39 | op.execute(sa.text('CREATE extension IF NOT EXISTS earthdistance;')) 40 | 41 | 42 | def downgrade(): 43 | op.drop_column('operators', 'custom') 44 | op.drop_column('chargers', 'custom') 45 | op.drop_table('geofences') 46 | -------------------------------------------------------------------------------- /.github/workflows/compose.yml: -------------------------------------------------------------------------------- 1 | name: Docker-Compose CI 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the master branch 5 | push: 6 | branches: [ main ] 7 | paths: 8 | - '.github/workflows/compose.yml' 9 | - 'docker-compose*.yml' 10 | - '.env' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | compose-file: [docker-compose.yml, docker-compose-homekit-host.yml, docker-compose-homekit-macvlan.yml] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Build the docker-compose stack 21 | run: docker-compose -f ${{ matrix.compose-file }} --env-file .env up -d 22 | - name: Container Status 23 | run: docker ps -a 24 | - name: Let containers run for 60s 25 | uses: juliangruber/sleep-action@v2 26 | with: 27 | time: 60s 28 | - name: Check logs for vwsfriend 29 | run: | 30 | docker logs vwsfriend_vwsfriend_1 31 | docker logs vwsfriend_vwsfriend_1 2>&1 | grep -q 'CRITICAL:vwsfriend_base:There was a problem when authenticating with WeConnect: Your account for test@test.de was not found. Would you like to create a new account?\|CRITICAL:vwsfriend_base:There was a problem when authenticating with WeConnect: Login throttled, probably too many wrong logins. You have to wait some minutes until a new login attempt is possible' 32 | - name: Check logs for postgresdb 33 | run: docker logs vwsfriend_postgresdb_1 34 | - name: Check logs for grafana 35 | run: docker logs vwsfriend_grafana_1 36 | - name: Container Status 37 | run: docker ps -a 38 | - name: Check running containers again 39 | run: | 40 | docker ps -a | grep -q 'Up.*\(healthy\).*vwsfriend_grafana_1' 41 | docker ps -a | grep -q 'Up.*\(healthy\).*vwsfriend_postgresdb_1' 42 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/refuel_session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, BigInteger, Float, String, ForeignKey, Table 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | refuel_tag_association_table = Table('refuel_tag', Base.metadata, 9 | Column('refuel_sessions_id', ForeignKey('refuel_sessions.id')), 10 | Column('tag_name', ForeignKey('tag.name')) 11 | ) 12 | 13 | 14 | class RefuelSession(Base): 15 | __tablename__ = 'refuel_sessions' 16 | id = Column(Integer, primary_key=True) 17 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 18 | vehicle = relationship("Vehicle") 19 | 20 | date = Column(DatetimeDecorator) 21 | startSOC_pct = Column(Integer) 22 | endSOC_pct = Column(Integer) 23 | mileage_km = Column(Integer) 24 | position_latitude = Column(Float) 25 | position_longitude = Column(Float) 26 | location_id = Column(BigInteger, ForeignKey('locations.osm_id')) 27 | location = relationship("Location") 28 | realRefueled_l = Column(Float) 29 | realCost_ct = Column(Integer) 30 | tags = relationship("Tag", secondary=refuel_tag_association_table, backref=backref("refuel_sessions")) 31 | 32 | def __init__(self, vehicle, date, startSOC_pct, endSOC_pct, mileage_km, position_latitude, position_longitude, location): 33 | self.vehicle = vehicle 34 | self.date = date 35 | self.startSOC_pct = startSOC_pct 36 | self.endSOC_pct = endSOC_pct 37 | self.mileage_km = mileage_km 38 | self.position_latitude = position_latitude 39 | self.position_longitude = position_longitude 40 | self.location = location 41 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_trip/003_0s_driving.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "measurements": { 14 | "odometerStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(600)", 17 | "odometer": 4166 18 | } 19 | } 20 | } 21 | }, 22 | "now(+0)" 23 | ], 24 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 25 | { 26 | "data": {} 27 | }, 28 | "now(+0)" 29 | ], 30 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 31 | { 32 | "data": [ 33 | { 34 | "vin": "AAABBBCCCDDD12345", 35 | "role": "PRIMARY_USER", 36 | "enrollmentStatus": "COMPLETED", 37 | "model": "Demo Test Vehicle", 38 | "nickname": "Test Vehicle", 39 | "capabilities": [{ 40 | "id": "parkingPosition", 41 | "expirationDate": "2051-05-12T11:53:00Z", 42 | "userDisablingAllowed": false 43 | }], 44 | "images": {}, 45 | "coUsers": [] 46 | } 47 | ] 48 | }, 49 | "now(+0)" 50 | ] 51 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/flashing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from threading import Timer 3 | 4 | import pyhap 5 | 6 | from weconnect.errors import SetterError 7 | 8 | from vwsfriend.homekit.genericAccessory import GenericAccessory 9 | 10 | LOG = logging.getLogger("VWsFriend") 11 | 12 | 13 | class Flashing(GenericAccessory): 14 | """Flashing Accessory""" 15 | 16 | category = pyhap.const.CATEGORY_LIGHTBULB 17 | 18 | def __init__(self, driver, bridge, aid, id, vin, displayName, flashControl): 19 | super().__init__(driver=driver, bridge=bridge, displayName=displayName, aid=aid, vin=vin, id=id) 20 | 21 | self.flashControl = flashControl 22 | self.service = self.add_preload_service('Lightbulb', ['Name', 'ConfiguredName', 'On']) 23 | 24 | self.charOn = self.service.configure_char('On', setter_callback=self.__onOnChanged) 25 | 26 | self.addNameCharacteristics() 27 | 28 | def __onOnChanged(self, value): 29 | if self.flashControl is not None and self.flashControl.enabled: 30 | try: 31 | if value is True: 32 | LOG.info('Start flashing for 10 seconds') 33 | self.flashControl.value = 10 34 | 35 | def resetState(): 36 | self.charOn.set_value(False) 37 | 38 | timer = Timer(10.0, resetState) 39 | timer.start() 40 | else: 41 | LOG.error('Flashing cannot be turned off, please wait for 10 seconds') 42 | except SetterError as setterError: 43 | LOG.error('Error flashing: %s', setterError) 44 | self.charOn.set_value(False) 45 | self.setStatusFault(1, timeout=120) 46 | else: 47 | LOG.error('Flashing cannot be controlled') 48 | self.charOn.set_value(False) 49 | self.setStatusFault(1, timeout=120) 50 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% block title %}{% endblock %} - VWsFriend 3 | 4 | 5 | 6 | 7 | {% block head %} 8 | {% endblock %} 9 |
    10 | 33 |
    34 |
    35 | {% block header %}{% endblock %} 36 |
    37 | {% for message in get_flashed_messages() %} 38 |
    {{ message }}
    39 | {% endfor %} 40 | {% block content %}{% endblock %} 41 |
    42 | 43 | 48 |
    -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/settings/abrpsettings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}ABRP Settings for {{vehicle.nickname.value}} {% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    To connect this vehicle to ABRP you have to generate a user token for this vehicle. To support multiple ABRP accounts you can set multiple tokens here.

    9 |
    10 |
    11 | {{ form.csrf_token }} 12 | 13 | {% for a in form.accounts %} 14 |
    15 |
    16 | {{ a.form.account_name.label }} 17 | {{ a.form.account_name(size=20) }} 18 | {% if a.form.account_name.errors %} 19 |
      20 | {% for error in a.form.account_name.errors %} 21 |
    • {{ error }}
    • 22 | {% endfor %} 23 |
    24 | {% endif %} 25 |
    26 |
    27 | {{ a.form.account_token.label }} 28 | {{ a.form.account_token(size=20) }} 29 | {% if a.form.account_token.errors %} 30 |
      31 | {% for error in a.form.account_token.errors %} 32 |
    • {{ error }}
    • 33 | {% endfor %} 34 |
    35 | {% endif %} 36 |
    37 | {{ a.form.delete }} 38 |
    39 | {% endfor %} 40 | {% if form.accounts.errors %} 41 |
      42 | {% for error in form.accounts.errors %} 43 |
    • {{ error }}
    • 44 | {% endfor %} 45 |
    46 | {% endif %} 47 | 48 | {{ form.addAccount }}{{ form.save }} 49 | 50 |
    51 |
    52 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/overview.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}Overview{% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 |

    Geofence

    9 | 13 | 14 |

    Trip

    15 | 19 | 20 |

    Charging

    21 | 25 | 29 | 33 | 34 |

    Refueling

    35 | 39 | 40 |

    Journeys

    41 | 45 | 46 |

    Tags

    47 | 51 | 52 |

    Settings

    53 | 56 | 57 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_trip/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "measurements": { 14 | "odometerStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(-300)", 17 | "odometer": 4166 18 | } 19 | } 20 | } 21 | }, 22 | "now(+0)" 23 | ], 24 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 25 | { 26 | "data": { 27 | "lat": 53.60957750612437, 28 | "lon": 10.184564007135274, 29 | "carCapturedTimestamp": "demodate(300)" 30 | } 31 | }, 32 | "now(+0)" 33 | ], 34 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 35 | { 36 | "data": [ 37 | { 38 | "vin": "AAABBBCCCDDD12345", 39 | "role": "PRIMARY_USER", 40 | "enrollmentStatus": "COMPLETED", 41 | "model": "Demo Test Vehicle", 42 | "nickname": "Test Vehicle", 43 | "capabilities": [{ 44 | "id": "parkingPosition", 45 | "expirationDate": "2051-05-12T11:53:00Z", 46 | "userDisablingAllowed": false 47 | }], 48 | "images": {}, 49 | "coUsers": [] 50 | } 51 | ] 52 | }, 53 | "now(+0)" 54 | ] 55 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_trip/002_0s_parking.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "measurements": { 14 | "odometerStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(300)", 17 | "odometer": 4166 18 | } 19 | } 20 | } 21 | }, 22 | "now(+0)" 23 | ], 24 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 25 | { 26 | "data": { 27 | "lat": 53.60957750612437, 28 | "lon": 10.184564007135274, 29 | "carCapturedTimestamp": "demodate(300)" 30 | } 31 | }, 32 | "now(+0)" 33 | ], 34 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 35 | { 36 | "data": [ 37 | { 38 | "vin": "AAABBBCCCDDD12345", 39 | "role": "PRIMARY_USER", 40 | "enrollmentStatus": "COMPLETED", 41 | "model": "Demo Test Vehicle", 42 | "nickname": "Test Vehicle", 43 | "capabilities": [{ 44 | "id": "parkingPosition", 45 | "expirationDate": "2051-05-12T11:53:00Z", 46 | "userDisablingAllowed": false 47 | }], 48 | "images": {}, 49 | "coUsers": [] 50 | } 51 | ] 52 | }, 53 | "now(+0)" 54 | ] 55 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_trip/004_0s_parking2.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "measurements": { 14 | "odometerStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(800)", 17 | "odometer": 4180 18 | } 19 | } 20 | } 21 | }, 22 | "now(+0)" 23 | ], 24 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 25 | { 26 | "data": { 27 | "lat": 53.60957750612437, 28 | "lon": 10.184564007135274, 29 | "carCapturedTimestamp": "demodate(900)" 30 | } 31 | }, 32 | "now(+0)" 33 | ], 34 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 35 | { 36 | "data": [ 37 | { 38 | "vin": "AAABBBCCCDDD12345", 39 | "role": "PRIMARY_USER", 40 | "enrollmentStatus": "COMPLETED", 41 | "model": "Demo Test Vehicle", 42 | "nickname": "Test Vehicle", 43 | "capabilities": [{ 44 | "id": "parkingPosition", 45 | "expirationDate": "2051-05-12T11:53:00Z", 46 | "userDisablingAllowed": false 47 | }], 48 | "images": {}, 49 | "coUsers": [] 50 | } 51 | ] 52 | }, 53 | "now(+0)" 54 | ] 55 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/trip.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, BigInteger, Float, String, ForeignKey, Table 2 | from sqlalchemy.orm import relationship, backref 3 | 4 | from vwsfriend.model.base import Base 5 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 6 | 7 | 8 | trip_tag_association_table = Table('trip_tag', Base.metadata, 9 | Column('trips_id', ForeignKey('trips.id')), 10 | Column('tag_name', ForeignKey('tag.name')) 11 | ) 12 | 13 | 14 | class Trip(Base): 15 | __tablename__ = 'trips' 16 | id = Column(Integer, primary_key=True) 17 | vehicle_vin = Column(String, ForeignKey('vehicles.vin')) 18 | vehicle = relationship("Vehicle") 19 | 20 | startDate = Column(DatetimeDecorator) 21 | endDate = Column(DatetimeDecorator) 22 | start_position_latitude = Column(Float) 23 | start_position_longitude = Column(Float) 24 | start_location_id = Column(BigInteger, ForeignKey('locations.osm_id')) 25 | start_location = relationship("Location", foreign_keys=[start_location_id]) 26 | destination_position_latitude = Column(Float) 27 | destination_position_longitude = Column(Float) 28 | destination_location_id = Column(BigInteger, ForeignKey('locations.osm_id')) 29 | destination_location = relationship("Location", foreign_keys=[destination_location_id]) 30 | start_mileage_km = Column(Integer) 31 | end_mileage_km = Column(Integer) 32 | tags = relationship("Tag", secondary=trip_tag_association_table, backref=backref("trips")) 33 | 34 | def __init__(self, vehicle, startDate, start_position_latitude, start_position_longitude, start_location, start_mileage_km): 35 | self.vehicle = vehicle 36 | self.startDate = startDate 37 | self.start_position_latitude = start_position_latitude 38 | self.start_position_longitude = start_position_longitude 39 | self.start_location = start_location 40 | self.start_mileage_km = start_mileage_km 41 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/status/vehicles.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

    {% block title %}Vehicles{% endblock %}

    5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | {% for vehicle in vehicles %} 11 | 12 | 13 | 29 | 30 | 31 | {% endfor %} 32 |
    14 |

    {{vehicle.nickname.value}} ({{vehicle.model.value}})

    15 |

    16 | 17 | {% if vehicle.statusExists('charging', 'batteryStatus') and vehicle.domains['charging']['batteryStatus'].currentSOC_pct.enabled %} {{vehicle.domains['charging']['batteryStatus'].currentSOC_pct}}% electric SoC,{% endif %} 18 | {% if vehicle.statusExists('parking', 'parkingPosition') and vehicle.domains['parking']['parkingPosition'].enabled and not vehicle.domains['parking']['parkingPosition'].hasError() %} parked, {% endif %} 19 | {% if vehicle.statusExists('vehicleHealthWarnings', 'warningLights') and vehicle.domains['vehicleHealthWarnings']['warningLights'].enabled and vehicle.domains['vehicleHealthWarnings']['warningLights'].warningLights|length > 0 %} 20 |
    21 | {% for warningLight in vehicle.domains['vehicleHealthWarnings']['warningLights'].warningLights.values() %} 22 | Warning: {{warningLight.text.value}}
    23 | {% endfor %} 24 | {% endif %} 25 | 26 |

    27 |
    28 |
    33 | {% endblock %} -------------------------------------------------------------------------------- /.github/workflows/build-vwsfriend-python.yml: -------------------------------------------------------------------------------- 1 | name: Build Python Package 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: [ main ] 8 | paths: 9 | - .github/workflows/build-vwsfriend-python.yml 10 | - 'vwsfriend/**.py' 11 | - 'vwsfriend/**requirements.txt' 12 | pull_request: 13 | paths: 14 | - .github/workflows/build-vwsfriend-python.yml 15 | - 'vwsfriend/**.py' 16 | - 'vwsfriend/**requirements.txt' 17 | 18 | jobs: 19 | build-python: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] 24 | with-mqtt: [true, false] 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | working-directory: vwsfriend 34 | run: | 35 | python -m pip install --upgrade pip 36 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 37 | if [ -f setup_requirements.txt ]; then pip install -r setup_requirements.txt; fi 38 | if [ -f test_requirements.txt ]; then pip install -r test_requirements.txt; fi 39 | - name: Install mqtt dependencies 40 | working-directory: vwsfriend 41 | if: matrix.with-mqtt == 'true' 42 | run: | 43 | if [ -f mqtt_extra_requirements.txt ]; then pip install -r mqtt_extra_requirements.txt; fi 44 | - name: Lint 45 | working-directory: vwsfriend 46 | run: | 47 | make lint 48 | - name: Test 49 | working-directory: vwsfriend 50 | run: | 51 | make test 52 | - name: Archive code coverage results 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: code-coverage-report-${{ matrix.os }}-${{ matrix.python-version }} 56 | path: coverage_html_report/** -------------------------------------------------------------------------------- /grafana/public/img/icons/parking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /grafana/public/img/icons/charger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/settings_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 7 | 10 | 11 | 12 | 13 | 14 | {% endblock %} 15 | 16 | {% block header %} 17 |

    {% block title %}Edit Settings{% endblock %}

    18 | {% endblock %} 19 | 20 | {% block content %} 21 |

    In order for VWsFriend to work we would like you to review and adjust these settings:

    22 | {% if form %} 23 |
    24 |
    25 | {{ form.csrf_token }} 26 | 27 | {{ form.id }} 28 | {{ form.unit_of_length }} 29 | {{ form.unit_of_temperature }} 30 | {{ form.locale }} 31 | 32 |
    33 | {{ form.vwsfriend_url.label }} 34 | {{ form.vwsfriend_url }} 35 | {% if form.vwsfriend_url.errors %} 36 |
      37 | {% for error in form.vwsfriend_url.errors %} 38 |
    • {{ error }}
    • 39 | {% endfor %} 40 |
    41 | {% endif %} 42 |
    43 | 44 |
    45 | {{ form.grafana_url.label }} 46 | {{ form.grafana_url }} 47 | {% if form.grafana_url.errors %} 48 |
      49 | {% for error in form.grafana_url.errors %} 50 |
    • {{ error }}
    • 51 | {% endfor %} 52 |
    53 | {% endif %} 54 |
    55 | 56 | {{ form.save }} 57 |
    58 |
    59 | {% endif %} 60 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/agents/weconnect_error_agent.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone, timedelta 2 | import logging 3 | from sqlalchemy.exc import IntegrityError 4 | 5 | from vwsfriend.model.weconnect_error import WeConnectError 6 | from vwsfriend.model.weconnect_responsetime import WeConnectResponsetime 7 | 8 | from weconnect.weconnect import WeConnect 9 | from weconnect.weconnect_errors import ErrorEventType 10 | 11 | LOG = logging.getLogger("VWsFriend") 12 | 13 | 14 | class WeconnectErrorAgent(): 15 | def __init__(self, session, weconnect: WeConnect): 16 | self.session = session 17 | self.weconnect = weconnect 18 | 19 | # register for updates: 20 | weconnect.addErrorObserver(self.__onError, ErrorEventType.ALL) 21 | 22 | def __onError(self, element, errortype, detail, message): 23 | error = WeConnectError(datetime.utcnow().replace(tzinfo=timezone.utc), errortype, detail) 24 | with self.session.begin_nested(): 25 | try: 26 | self.session.add(error) 27 | except IntegrityError: 28 | LOG.warning('Could not add error entry to the database') 29 | self.session.commit() 30 | 31 | def commit(self): 32 | min = self.weconnect.getMinElapsed() 33 | if min is not None: 34 | min /= timedelta(microseconds=1) 35 | avg = self.weconnect.getAvgElapsed() 36 | if avg is not None: 37 | avg /= timedelta(microseconds=1) 38 | max = self.weconnect.getMaxElapsed() 39 | if max is not None: 40 | max /= timedelta(microseconds=1) 41 | total = self.weconnect.getTotalElapsed() 42 | if total is not None: 43 | total /= timedelta(microseconds=1) 44 | responsetime = WeConnectResponsetime(datetime.utcnow().replace(tzinfo=timezone.utc), min, avg, max, total) 45 | with self.session.begin_nested(): 46 | try: 47 | self.session.add(responsetime) 48 | except IntegrityError: 49 | LOG.warning('Could not add responsetime entry to the database') 50 | self.session.commit() 51 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/versions/b7c0b285b1f3_add_tags.py: -------------------------------------------------------------------------------- 1 | """Add tags 2 | 3 | Revision ID: b7c0b285b1f3 4 | Revises: ae1799a66935 5 | Create Date: 2022-02-21 16:04:53.710946 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b7c0b285b1f3' 14 | down_revision = 'ae1799a66935' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('tag', 21 | sa.Column('name', sa.String(), nullable=False), 22 | sa.Column('description', sa.String(), nullable=True), 23 | sa.Column('use_trips', sa.Boolean(), nullable=False), 24 | sa.Column('use_charges', sa.Boolean(), nullable=False), 25 | sa.Column('use_refueling', sa.Boolean(), nullable=False), 26 | sa.PrimaryKeyConstraint('name') 27 | ) 28 | op.create_table('refuel_tag', 29 | sa.Column('refuel_sessions_id', sa.Integer(), nullable=True), 30 | sa.Column('tag_name', sa.String(), nullable=True), 31 | sa.ForeignKeyConstraint(['refuel_sessions_id'], ['refuel_sessions.id'], ), 32 | sa.ForeignKeyConstraint(['tag_name'], ['tag.name'], ) 33 | ) 34 | op.create_table('trip_tag', 35 | sa.Column('trips_id', sa.Integer(), nullable=True), 36 | sa.Column('tag_name', sa.String(), nullable=True), 37 | sa.ForeignKeyConstraint(['tag_name'], ['tag.name'], ), 38 | sa.ForeignKeyConstraint(['trips_id'], ['trips.id'], ) 39 | ) 40 | op.create_table('charging_tag', 41 | sa.Column('charging_sessions_id', sa.Integer(), nullable=True), 42 | sa.Column('tag_name', sa.String(), nullable=True), 43 | sa.ForeignKeyConstraint(['charging_sessions_id'], ['charging_sessions.id'], ), 44 | sa.ForeignKeyConstraint(['tag_name'], ['tag.name'], ) 45 | ) 46 | 47 | 48 | def downgrade(): 49 | op.drop_table('charging_tag') 50 | op.drop_table('trip_tag') 51 | op.drop_table('refuel_tag') 52 | op.drop_table('tag') 53 | -------------------------------------------------------------------------------- /vwsfriend/setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | README = (HERE / "README.md").read_text() 8 | INSTALL_REQUIRED = (HERE / "requirements.txt").read_text() 9 | SETUP_REQUIRED = (HERE / "setup_requirements.txt").read_text() 10 | TEST_REQUIRED = (HERE / "test_requirements.txt").read_text() 11 | MQTT_EXTRA_REQUIRED = (HERE / "mqtt_extra_requirements.txt").read_text() 12 | 13 | setup( 14 | name='vwsfriend', 15 | packages=find_packages(), 16 | version=open("vwsfriend/__version.py").readlines()[-1].split()[-1].strip("\"'"), 17 | description='', 18 | long_description=README, 19 | long_description_content_type="text/markdown", 20 | author='Till Steinbach', 21 | keywords='weconnect, we connect, carnet, car net, volkswagen, vw, telemetry, smarthome', 22 | url='https://github.com/tillsteinbach/VWsFriend', 23 | project_urls={ 24 | 'Funding': 'https://github.com/sponsors/VWsFriend', 25 | 'Source': 'https://github.com/tillsteinbach/VWsFriend', 26 | 'Bug Tracker': 'https://github.com/tillsteinbach/VWsFriend/issues' 27 | }, 28 | license='MIT', 29 | install_requires=INSTALL_REQUIRED, 30 | extras_require={ 31 | "MQTT": MQTT_EXTRA_REQUIRED, 32 | }, 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'vwsfriend = vwsfriend.vwsfriend_base:main', 36 | ], 37 | }, 38 | classifiers=[ 39 | 'Development Status :: 3 - Alpha', 40 | 'Environment :: Console', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Intended Audience :: End Users/Desktop', 43 | 'Intended Audience :: System Administrators', 44 | 'Programming Language :: Python :: 3.9', 45 | 'Programming Language :: Python :: 3.10', 46 | 'Programming Language :: Python :: 3.11', 47 | 'Programming Language :: Python :: 3.12', 48 | 'Programming Language :: Python :: 3.13', 49 | 'Programming Language :: Python :: 3.14', 50 | 'Topic :: Utilities', 51 | 'Topic :: System :: Monitoring', 52 | 'Topic :: Home Automation', 53 | ], 54 | python_requires='>=3.9', 55 | setup_requires=SETUP_REQUIRED, 56 | tests_require=TEST_REQUIRED, 57 | include_package_data=True, 58 | zip_safe=False, 59 | ) 60 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vwsfriend-schema/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | from vwsfriend.model.base import Base 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | if config.attributes.get('configure_logger', True): 17 | fileConfig(config.config_file_name) 18 | 19 | target_metadata = Base.metadata 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure( 41 | url=url, 42 | target_metadata=target_metadata, 43 | literal_binds=True, 44 | dialect_opts={"paramstyle": "named"}, 45 | compare_type=True, 46 | ) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | def run_migrations_online(): 53 | """Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine 56 | and associate a connection with the context. 57 | 58 | """ 59 | connectable = engine_from_config( 60 | config.get_section(config.config_ini_section), 61 | prefix="sqlalchemy.", 62 | poolclass=pool.NullPool, 63 | ) 64 | 65 | with connectable.connect() as connection: 66 | context.configure( 67 | connection=connection, target_metadata=target_metadata, compare_type=True 68 | ) 69 | 70 | with context.begin_transaction(): 71 | context.run_migrations() 72 | 73 | 74 | if context.is_offline_mode(): 75 | run_migrations_offline() 76 | else: 77 | run_migrations_online() 78 | -------------------------------------------------------------------------------- /.github/workflows/vwsfriend-docker-edge.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build VWsFriend docker edge 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ main ] 10 | paths: 11 | - .github/workflows/vwsfriend-docker-edge.yml 12 | - vwsfriend/** 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | vwsfriend: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Set Swap Space 20 | uses: pierotofy/set-swap-space@v1.0 21 | with: 22 | swap-size-gb: 10 23 | - run: | 24 | # Workaround for https://github.com/rust-lang/cargo/issues/8719 25 | sudo mkdir -p /var/lib/docker 26 | sudo mount -t tmpfs -o size=10G none /var/lib/docker 27 | sudo systemctl restart docker 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Docker meta 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: | 35 | tillsteinbach/vwsfriend 36 | ghcr.io/tillsteinbach/vwsfriend 37 | tags: | 38 | type=edge 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v3 41 | - name: Setup Docker Buildx 42 | uses: docker/setup-buildx-action@v3.6.1 43 | - name: Login to DockerHub 44 | uses: docker/login-action@v3.3.0 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USERNAME }} 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | - name: Login to GitHub Container Registry 49 | uses: docker/login-action@v3.3.0 50 | with: 51 | registry: ghcr.io 52 | username: ${{ github.repository_owner }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | - name: Build and push 55 | id: docker_build 56 | uses: docker/build-push-action@v6.7.0 57 | with: 58 | allow: security.insecure 59 | context: vwsfriend 60 | file: vwsfriend/Dockerfile-edge 61 | push: ${{ github.event_name != 'pull_request' }} 62 | platforms: linux/amd64,linux/arm/v7,linux/arm64 63 | tags: ${{ steps.meta.outputs.tags }} 64 | labels: ${{ steps.meta.outputs.labels }} 65 | cache-from: type=gha 66 | cache-to: type=gha,mode=max 67 | -------------------------------------------------------------------------------- /vwsfriend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.github/workflows/vwsfriend-docker-experimental.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build VWsFriend docker experimental.yml 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ experimental ] 10 | paths: 11 | - .github/workflows/vwsfriend-docker-experimental.yml 12 | - vwsfriend/** 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | vwsfriend: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Set Swap Space 20 | uses: pierotofy/set-swap-space@v1.0 21 | with: 22 | swap-size-gb: 10 23 | - run: | 24 | # Workaround for https://github.com/rust-lang/cargo/issues/8719 25 | sudo mkdir -p /var/lib/docker 26 | sudo mount -t tmpfs -o size=10G none /var/lib/docker 27 | sudo systemctl restart docker 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Docker meta 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: | 35 | tillsteinbach/vwsfriend 36 | ghcr.io/tillsteinbach/vwsfriend 37 | tags: | 38 | raw=raw,value=experimental 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v3 41 | - name: Setup Docker Buildx 42 | uses: docker/setup-buildx-action@v3.6.1 43 | - name: Login to DockerHub 44 | uses: docker/login-action@v3.3.0 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USERNAME }} 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | - name: Login to GitHub Container Registry 49 | uses: docker/login-action@v3.3.0 50 | with: 51 | registry: ghcr.io 52 | username: ${{ github.repository_owner }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | - name: Build and push 55 | id: docker_build 56 | uses: docker/build-push-action@v6.7.0 57 | with: 58 | context: vwsfriend 59 | file: vwsfriend/Dockerfile-edge 60 | push: ${{ github.event_name != 'pull_request' }} 61 | platforms: linux/amd64,linux/arm/v7,linux/arm64 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | cache-from: type=gha 65 | cache-to: type=gha,mode=max 66 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/plug.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyhap 4 | 5 | from weconnect.addressable import AddressableLeaf 6 | from weconnect.elements.plug_status import PlugStatus 7 | 8 | from vwsfriend.homekit.genericAccessory import GenericAccessory 9 | 10 | LOG = logging.getLogger("VWsFriend") 11 | 12 | 13 | class Plug(GenericAccessory): 14 | """Plug Accessory""" 15 | 16 | category = pyhap.const.CATEGORY_SENSOR 17 | 18 | def __init__(self, driver, bridge, aid, id, vin, displayName, plugStatus): 19 | super().__init__(driver=driver, bridge=bridge, displayName=displayName, aid=aid, vin=vin, id=id) 20 | 21 | self.service = self.add_preload_service('ContactSensor', ['Name', 'ConfiguredName', 'ContactSensorState', 'StatusFault']) 22 | 23 | if plugStatus is not None and plugStatus.plugConnectionState.enabled: 24 | plugStatus.plugConnectionState.addObserver(self.onplugConnectionStateChange, AddressableLeaf.ObserverEvent.VALUE_CHANGED) 25 | self.charContactSensorState = self.service.configure_char('ContactSensorState') 26 | self.charStatusFault = self.service.configure_char('StatusFault') 27 | self.setContactSensorState(plugStatus.plugConnectionState) 28 | 29 | self.addNameCharacteristics() 30 | 31 | def setContactSensorState(self, plugConnectionState): 32 | if self.charContactSensorState is not None: 33 | if plugConnectionState.value == PlugStatus.PlugConnectionState.CONNECTED: 34 | self.charContactSensorState.set_value(0) 35 | self.charStatusFault.set_value(0) 36 | elif plugConnectionState.value in (PlugStatus.PlugConnectionState.DISCONNECTED, 37 | PlugStatus.PlugConnectionState.UNSUPPORTED, 38 | PlugStatus.PlugConnectionState.INVALID): 39 | self.charContactSensorState.set_value(1) 40 | self.charStatusFault.set_value(0) 41 | else: 42 | self.charContactSensorState.set_value(0) 43 | self.charStatusFault.set_value(1) 44 | LOG.warn('unsupported plugConnectionState: %s', plugConnectionState.value.value) 45 | 46 | def onplugConnectionStateChange(self, element, flags): 47 | if flags & AddressableLeaf.ObserverEvent.VALUE_CHANGED: 48 | self.setContactSensorState(element) 49 | LOG.debug('Plug connection state Changed: %s', element.value.value) 50 | else: 51 | LOG.debug('Unsupported event %s', flags) 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '24 9 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/journey_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block header %} 7 |

    {% block title %}{% if form and form.id.data %}Edit settings for {% else %}Add a new {% endif %}journey{% endblock %}

    8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if form %} 12 |
    13 |
    14 | {{ form.csrf_token }} 15 | 16 | {{ form.id }} 17 | {{ form.vehicle_vin }} 18 | 19 |
    20 | {{ form.start.label }} 21 | {{ form.start }} 22 | {% if form.start.errors %} 23 |
      24 | {% for error in form.start.errors %} 25 |
    • {{ error }}
    • 26 | {% endfor %} 27 |
    28 | {% endif %} 29 |
    30 | 31 |
    32 | {{ form.end.label }} 33 | {{ form.end }} 34 | {% if form.end.errors %} 35 |
      36 | {% for error in form.end.errors %} 37 |
    • {{ error }}
    • 38 | {% endfor %} 39 |
    40 | {% endif %} 41 |
    42 | 43 |
    44 | {{ form.title.label }} 45 | {{ form.title }} 46 | {% if form.title.errors %} 47 |
      48 | {% for error in form.title.errors %} 49 |
    • {{ error }}
    • 50 | {% endfor %} 51 |
    52 | {% endif %} 53 |
    54 | 55 |
    56 | {{ form.description.label }} 57 | {{ form.description }} 58 | {% if form.description.errors %} 59 |
      60 | {% for error in form.description.errors %} 61 |
    • {{ error }}
    • 62 | {% endfor %} 63 |
    64 | {% endif %} 65 |
    66 | 67 |
    68 | {{ form.tags.label }} 69 | {{ form.tags }} 70 | {% if form.tags.errors %} 71 |
      72 | {% for error in form.tags.errors %} 73 |
    • {{ error }}
    • 74 | {% endfor %} 75 |
    76 | {% endif %} 77 | Click to select, ctrl-click to unselect. 78 |
    79 | 80 | {% if form.id.data %} 81 | {{ form.save }} 82 | {% else %} 83 | {{ form.add }} 84 | {% endif %} 85 | {{ form.delete }} 86 | 87 |
    88 |
    89 | {% endif %} 90 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel/003_0s_refuelDone.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "fuelStatus": { 14 | "rangeStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(300)", 17 | "carType": "hybrid", 18 | "primaryEngine": { 19 | "type": "gasoline", 20 | "currentSOC_pct": 100, 21 | "remainingRange_km": 550 22 | }, 23 | "secondaryEngine": { 24 | "type": "electric", 25 | "currentSOC_pct": 64, 26 | "remainingRange_km": 35 27 | }, 28 | "totalRange_km": 585 29 | } 30 | } 31 | }, 32 | "measurements": { 33 | "odometerStatus": { 34 | "value": { 35 | "carCapturedTimestamp": "demodate(0)", 36 | "odometer": 4166 37 | } 38 | } 39 | } 40 | }, 41 | "now(+0)" 42 | ], 43 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 44 | {}, 45 | "now(+0)" 46 | ], 47 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 48 | { 49 | "data": [ 50 | { 51 | "vin": "AAABBBCCCDDD12345", 52 | "role": "PRIMARY_USER", 53 | "enrollmentStatus": "COMPLETED", 54 | "model": "Demo Test Vehicle", 55 | "nickname": "Test Vehicle", 56 | "capabilities": [{ 57 | "id": "parkingPosition", 58 | "expirationDate": "2051-05-12T11:53:00Z", 59 | "userDisablingAllowed": false 60 | }], 61 | "images": {}, 62 | "coUsers": [] 63 | } 64 | ] 65 | }, 66 | "now(+0)" 67 | ] 68 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_position_before/003_0s_refuelDone.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "fuelStatus": { 14 | "rangeStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(300)", 17 | "carType": "hybrid", 18 | "primaryEngine": { 19 | "type": "gasoline", 20 | "currentSOC_pct": 100, 21 | "remainingRange_km": 550 22 | }, 23 | "secondaryEngine": { 24 | "type": "electric", 25 | "currentSOC_pct": 64, 26 | "remainingRange_km": 35 27 | }, 28 | "totalRange_km": 585 29 | } 30 | } 31 | }, 32 | "measurements": { 33 | "odometerStatus": { 34 | "value": { 35 | "carCapturedTimestamp": "demodate(0)", 36 | "odometer": 4166 37 | } 38 | } 39 | } 40 | }, 41 | "now(+0)" 42 | ], 43 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 44 | { 45 | "data": {} 46 | }, 47 | "now(+0)" 48 | ], 49 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 50 | { 51 | "data": [ 52 | { 53 | "vin": "AAABBBCCCDDD12345", 54 | "role": "PRIMARY_USER", 55 | "enrollmentStatus": "COMPLETED", 56 | "model": "Demo Test Vehicle", 57 | "nickname": "Test Vehicle", 58 | "capabilities": [{ 59 | "id": "parkingPosition", 60 | "expirationDate": "2051-05-12T11:53:00Z", 61 | "userDisablingAllowed": false 62 | }], 63 | "images": {}, 64 | "coUsers": [] 65 | } 66 | ] 67 | }, 68 | "now(+0)" 69 | ] 70 | } -------------------------------------------------------------------------------- /vwsfriend/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | # Here is the build image 3 | FROM ubuntu:22.04 as basebuilder 4 | 5 | ARG DEBIAN_FRONTEND="noninteractive" 6 | ENV TZ="Etc/UTC" 7 | 8 | RUN apt-get update && apt-get install --no-install-recommends -y pkg-config gpg gpg-agent software-properties-common && add-apt-repository ppa:deadsnakes/ppa && apt-get update && \ 9 | apt-get install --no-install-recommends -y python3.12 python3.12-dev python3.12-venv python3-pip python3-wheel build-essential && \ 10 | apt-get install --no-install-recommends -y libpq-dev libffi-dev libssl-dev rustc cargo libjpeg-dev zlib1g-dev && \ 11 | apt-get clean && rm -rf /var/lib/apt/lists/* 12 | 13 | FROM basebuilder as builder 14 | ARG VERSION 15 | 16 | RUN python3.12 -m venv /opt/venv 17 | # Make sure we use the virtualenv: 18 | ENV PATH="/opt/venv/bin:$PATH" 19 | RUN pip3 install --no-cache-dir wheel 20 | # This line is a workaround necessary for linux/arm/v7 architecture that has a bug with qemu: https://github.com/rust-lang/cargo/issues/8719 21 | # RUN --security=insecure mkdir -p /root/.cargo/registry/index && chmod 777 /root/.cargo/registry/index && mount -t tmpfs none /root/.cargo/registry/index && pip3 install --no-cache-dir cryptography 22 | RUN pip3 install --no-cache-dir vwsfriend[MQTT]==${VERSION} 23 | 24 | 25 | FROM ubuntu:22.04 AS baserunner 26 | 27 | ARG DEBIAN_FRONTEND="noninteractive" 28 | 29 | RUN apt-get update && apt-get install --no-install-recommends -y gpg gpg-agent software-properties-common && add-apt-repository ppa:deadsnakes/ppa && apt-get update && \ 30 | apt-get install --no-install-recommends -y python3.12 python3.12-venv wget && \ 31 | apt-get install --no-install-recommends -y libpq5 libjpeg8 zlib1g postgresql-client && \ 32 | apt-get clean && rm -rf /var/lib/apt/lists/* 33 | 34 | FROM baserunner AS runner-image 35 | 36 | ARG DEBIAN_FRONTEND="noninteractive" 37 | 38 | ENV VWSFRIEND_USERNAME= 39 | ENV VWSFRIEND_PASSWORD= 40 | ENV VWSFRIEND_PORT=4000 41 | ENV WECONNECT_USER= 42 | ENV WECONNECT_PASSWORD= 43 | ENV WECONNECT_SPIN= 44 | ENV WECONNECT_INTERVAL=300 45 | ENV ADDITIONAL_PARAMETERS= 46 | ENV DATABASE_URL= 47 | 48 | COPY --from=builder /opt/venv /opt/venv 49 | ENV VIRTUAL_ENV=/opt/venv 50 | ENV PATH="/opt/venv/bin:$PATH" 51 | RUN mkdir -p /config 52 | 53 | # make sure all messages always reach console 54 | ENV PYTHONUNBUFFERED=1 55 | 56 | EXPOSE 4000 57 | 58 | CMD vwsfriend --username=${VWSFRIEND_USERNAME} --password=${VWSFRIEND_PASSWORD} --weconnect-username=${WECONNECT_USER} --weconnect-password=${WECONNECT_PASSWORD} --weconnect-spin=${WECONNECT_SPIN} --interval=${WECONNECT_INTERVAL} --port=${VWSFRIEND_PORT} --database-url=${DATABASE_URL} --config-dir=/config ${ADDITIONAL_PARAMETERS} 59 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/homekit/genericAccessory.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import threading 4 | import logging 5 | 6 | import pyhap 7 | 8 | LOG = logging.getLogger("VWsFriend") 9 | 10 | 11 | class GenericAccessory(pyhap.accessory.Accessory): 12 | def __init__(self, driver, bridge, aid, id, vin, displayName): 13 | super().__init__(driver=driver, display_name=displayName, aid=aid) 14 | 15 | self.driver = driver 16 | self.bridge = bridge 17 | self.aid = aid 18 | self.id = id 19 | self.vin = vin 20 | self.displayName = displayName 21 | 22 | self.service = None 23 | 24 | self.charStatusFault = None 25 | self.__charStatusFaultTimer: Optional[threading.Timer] = None 26 | 27 | def addNameCharacteristics(self): 28 | configuredName = self.bridge.getConfigItem(self.id, self.vin, 'ConfiguredName') 29 | if configuredName is None: 30 | configuredName = self.displayName 31 | if self.service is not None: 32 | self.charConfiguredName = self.service.configure_char('ConfiguredName', value=configuredName, setter_callback=self.__onConfiguredNameChanged) 33 | self.charName = self.service.configure_char('Name', value=configuredName) 34 | 35 | def __onConfiguredNameChanged(self, value): 36 | if value is not None and len(value) > 0: 37 | self.bridge.setConfigItem(self.id, self.vin, 'ConfiguredName', value) 38 | self.bridge.persistConfig() 39 | self.charConfiguredName.set_value(value) 40 | self.charName.set_value(value) 41 | 42 | def addStatusFaultCharacteristic(self): 43 | if self.service is not None: 44 | self.charStatusFault = self.service.configure_char('StatusFault', value=0) 45 | 46 | def setStatusFault(self, value, timeout=0, timeoutValue=None): 47 | if self.charStatusFault is not None: 48 | if self.__charStatusFaultTimer is not None and self.__charStatusFaultTimer.is_alive(): 49 | self.__charStatusFaultTimer.cancel() 50 | self.__charStatusFaultTimer = None 51 | if timeout == 0: 52 | self.charStatusFault.set_value(value) 53 | else: 54 | if timeoutValue is None: 55 | timeoutValue = self.charStatusFault.get_value() 56 | self.charStatusFault.set_value(value) 57 | self.__charStatusFaultTimer = threading.Timer(timeout, self.__resetStatusFault, [timeoutValue]) 58 | self.__charStatusFaultTimer.daemon = True 59 | self.__charStatusFaultTimer.start() 60 | 61 | def __resetStatusFault(self, value): 62 | if self.charStatusFault is not None: 63 | self.charStatusFault.set_value(value) 64 | -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/model/vehicle.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Enum, Boolean 2 | from sqlalchemy.orm import relationship 3 | 4 | from weconnect.addressable import AddressableLeaf 5 | from weconnect.elements.range_status import RangeStatus 6 | 7 | from vwsfriend.model.base import Base 8 | from vwsfriend.model.datetime_decorator import DatetimeDecorator 9 | 10 | 11 | class Vehicle(Base): 12 | __tablename__ = 'vehicles' 13 | vin = Column(String(17), primary_key=True) 14 | model = Column(String(256)) 15 | nickname = Column(String(256)) 16 | carType = Column(Enum(RangeStatus.CarType)) 17 | online = Column(Boolean) 18 | lastUpdate = Column(DatetimeDecorator) 19 | lastChange = Column(DatetimeDecorator) 20 | settings = relationship("VehicleSettings", back_populates="vehicle", uselist=False) 21 | 22 | weConnectVehicle = None 23 | 24 | def __init__(self, vin): 25 | self.vin = vin 26 | 27 | def connect(self, weConnectVehicle): 28 | self.weConnectVehicle = weConnectVehicle 29 | self.weConnectVehicle.model.addObserver(self.__onModelChange, AddressableLeaf.ObserverEvent.VALUE_CHANGED) 30 | if self.weConnectVehicle.model.enabled and self.model != self.weConnectVehicle.model.value: 31 | self.model = self.weConnectVehicle.model.value 32 | self.weConnectVehicle.nickname.addObserver(self.__onNicknameChange, AddressableLeaf.ObserverEvent.VALUE_CHANGED) 33 | if self.weConnectVehicle.nickname.enabled and self.nickname != self.weConnectVehicle.nickname.value: 34 | self.nickname = self.weConnectVehicle.nickname.value 35 | if self.weConnectVehicle.statusExists('fuelStatus', 'rangeStatus') \ 36 | and self.weConnectVehicle.domains['fuelStatus']['rangeStatus'].enabled: 37 | self.weConnectVehicle.domains['fuelStatus']['rangeStatus'].carType.addObserver(self.__onCarTypeChange, AddressableLeaf.ObserverEvent.VALUE_CHANGED) 38 | if self.weConnectVehicle.domains['fuelStatus']['rangeStatus'].carType.enabled: 39 | self.carType = self.weConnectVehicle.domains['fuelStatus']['rangeStatus'].carType.value 40 | elif self.carType is None: 41 | self.carType = RangeStatus.CarType.UNKNOWN 42 | 43 | def __onModelChange(self, element, flags): 44 | if self.model != element.value: 45 | self.model = element.value 46 | 47 | def __onNicknameChange(self, element, flags): 48 | if self.nickname != element.value: 49 | self.nickname = element.value 50 | 51 | def __onCarTypeChange(self, element, flags): 52 | if self.carType != element.value: 53 | self.carType = element.value 54 | 55 | def displayString(self): # noqa: C901 56 | return f'{self.nickname} ({self.model})' 57 | -------------------------------------------------------------------------------- /vwsfriend/Dockerfile-edge: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | # Here is the build image 3 | FROM ubuntu:22.04 as basebuilder 4 | 5 | ARG DEBIAN_FRONTEND="noninteractive" 6 | ENV TZ="Etc/UTC" 7 | 8 | RUN apt-get update && apt-get install --no-install-recommends -y pkg-config gpg gpg-agent software-properties-common && add-apt-repository ppa:deadsnakes/ppa && apt-get update && \ 9 | apt-get install --no-install-recommends -y python3.12 python3.12-dev python3.12-venv python3-pip python3-wheel build-essential && \ 10 | apt-get install --no-install-recommends -y libpq-dev libffi-dev libssl-dev rustc cargo libjpeg-dev zlib1g-dev && \ 11 | apt-get clean && rm -rf /var/lib/apt/lists/* 12 | 13 | FROM basebuilder as builder 14 | COPY . vwsfriend 15 | RUN python3.12 -m venv /opt/venv 16 | # Make sure we use the virtualenv: 17 | ENV PATH="/opt/venv/bin:$PATH" 18 | WORKDIR ./vwsfriend/ 19 | RUN pip3 install --no-cache-dir wheel 20 | # This line is a workaround necessary for linux/arm/v7 architecture that has a bug with qemu: https://github.com/rust-lang/cargo/issues/8719 21 | # RUN --security=insecure mkdir -p /root/.cargo/registry/index && chmod 777 /root/.cargo/registry/index && mount -t tmpfs none /root/.cargo/registry/index && pip3 install --no-cache-dir cryptography 22 | RUN pip3 install --no-cache-dir -r requirements.txt 23 | RUN pip3 install --no-cache-dir -r mqtt_extra_requirements.txt 24 | RUN pip3 install --no-cache-dir . 25 | 26 | 27 | FROM ubuntu:22.04 AS baserunner 28 | 29 | ARG DEBIAN_FRONTEND="noninteractive" 30 | 31 | RUN apt-get update && apt-get install --no-install-recommends -y gpg gpg-agent software-properties-common && add-apt-repository ppa:deadsnakes/ppa && apt-get update && \ 32 | apt-get install --no-install-recommends -y python3.12 python3.12-venv wget && \ 33 | apt-get install --no-install-recommends -y libpq5 libjpeg8 zlib1g postgresql-client && \ 34 | apt-get clean && rm -rf /var/lib/apt/lists/* 35 | 36 | FROM baserunner AS runner-image 37 | 38 | ARG DEBIAN_FRONTEND="noninteractive" 39 | 40 | ENV VWSFRIEND_USERNAME= 41 | ENV VWSFRIEND_PASSWORD= 42 | ENV VWSFRIEND_PORT=4000 43 | ENV WECONNECT_USER= 44 | ENV WECONNECT_PASSWORD= 45 | ENV WECONNECT_SPIN= 46 | ENV WECONNECT_INTERVAL=300 47 | ENV ADDITIONAL_PARAMETERS= 48 | ENV DATABASE_URL= 49 | 50 | COPY --from=builder /opt/venv /opt/venv 51 | ENV VIRTUAL_ENV=/opt/venv 52 | ENV PATH="/opt/venv/bin:$PATH" 53 | RUN mkdir -p /config 54 | 55 | # make sure all messages always reach console 56 | ENV PYTHONUNBUFFERED=1 57 | 58 | EXPOSE 4000 59 | 60 | CMD vwsfriend --username=${VWSFRIEND_USERNAME} --password=${VWSFRIEND_PASSWORD} --weconnect-username=${WECONNECT_USER} --weconnect-password=${WECONNECT_PASSWORD} --weconnect-spin=${WECONNECT_SPIN} --interval=${WECONNECT_INTERVAL} --port=${VWSFRIEND_PORT} --database-url=${DATABASE_URL} --config-dir=/config ${ADDITIONAL_PARAMETERS} 61 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_splitted/003_0s_refuelPart.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "fuelStatus": { 14 | "rangeStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(300)", 17 | "carType": "hybrid", 18 | "primaryEngine": { 19 | "type": "gasoline", 20 | "currentSOC_pct": 50, 21 | "remainingRange_km": 260 22 | }, 23 | "secondaryEngine": { 24 | "type": "electric", 25 | "currentSOC_pct": 64, 26 | "remainingRange_km": 35 27 | }, 28 | "totalRange_km": 295 29 | } 30 | } 31 | }, 32 | "measurements": { 33 | "odometerStatus": { 34 | "value": { 35 | "carCapturedTimestamp": "demodate(0)", 36 | "odometer": 4166 37 | } 38 | } 39 | } 40 | }, 41 | "now(+0)" 42 | ], 43 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 44 | { 45 | "data": { 46 | "lat": 53.60853453781239, 47 | "lon": 10.19093520255688, 48 | "carCapturedTimestamp": "demodate(-300)" 49 | } 50 | }, 51 | "now(+0)" 52 | ], 53 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 54 | { 55 | "data": [ 56 | { 57 | "vin": "AAABBBCCCDDD12345", 58 | "role": "PRIMARY_USER", 59 | "enrollmentStatus": "COMPLETED", 60 | "model": "Demo Test Vehicle", 61 | "nickname": "Test Vehicle", 62 | "capabilities": [{ 63 | "id": "parkingPosition", 64 | "expirationDate": "2051-05-12T11:53:00Z", 65 | "userDisablingAllowed": false 66 | }], 67 | "images": {}, 68 | "coUsers": [] 69 | } 70 | ] 71 | }, 72 | "now(+0)" 73 | ] 74 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_splitted/004_0s_refuelDone.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 3 | { 4 | "userCapabilities": { 5 | "capabilitiesStatus": { 6 | "value": [{ 7 | "id": "parkingPosition", 8 | "expirationDate": "2051-05-12T11:53:00Z", 9 | "userDisablingAllowed": false 10 | }] 11 | } 12 | }, 13 | "fuelStatus": { 14 | "rangeStatus": { 15 | "value": { 16 | "carCapturedTimestamp": "demodate(600)", 17 | "carType": "hybrid", 18 | "primaryEngine": { 19 | "type": "gasoline", 20 | "currentSOC_pct": 100, 21 | "remainingRange_km": 550 22 | }, 23 | "secondaryEngine": { 24 | "type": "electric", 25 | "currentSOC_pct": 64, 26 | "remainingRange_km": 35 27 | }, 28 | "totalRange_km": 585 29 | } 30 | } 31 | }, 32 | "measurements": { 33 | "odometerStatus": { 34 | "value": { 35 | "carCapturedTimestamp": "demodate(0)", 36 | "odometer": 4166 37 | } 38 | } 39 | } 40 | }, 41 | "now(+0)" 42 | ], 43 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 44 | { 45 | "data": { 46 | "lat": 53.60853453781239, 47 | "lon": 10.19093520255688, 48 | "carCapturedTimestamp": "demodate(-300)" 49 | } 50 | }, 51 | "now(+0)" 52 | ], 53 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 54 | { 55 | "data": [ 56 | { 57 | "vin": "AAABBBCCCDDD12345", 58 | "role": "PRIMARY_USER", 59 | "enrollmentStatus": "COMPLETED", 60 | "model": "Demo Test Vehicle", 61 | "nickname": "Test Vehicle", 62 | "capabilities": [{ 63 | "id": "parkingPosition", 64 | "expirationDate": "2051-05-12T11:53:00Z", 65 | "userDisablingAllowed": false 66 | }], 67 | "images": {}, 68 | "coUsers": [] 69 | } 70 | ] 71 | }, 72 | "now(+0)" 73 | ] 74 | } -------------------------------------------------------------------------------- /vwsfriend/vwsfriend/ui/templates/database/tag_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | {% endblock %} 5 | 6 | {% block header %} 7 |

    {% block title %}{% if form and form.name.data %}Edit settings for {% else %}Add a new {% endif %}tag{% endblock %}

    8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if form %} 12 |
    13 |
    14 | {{ form.csrf_token }} 15 | 16 |
    17 | {{ form.name.label }} 18 | {{ form.name }} 19 | {% if form.name.errors %} 20 |
      21 | {% for error in form.name.errors %} 22 |
    • {{ error }}
    • 23 | {% endfor %} 24 |
    25 | {% endif %} 26 |
    27 | 28 |
    29 | {{ form.description.label }} 30 | {{ form.description }} 31 | {% if form.description.errors %} 32 |
      33 | {% for error in form.description.errors %} 34 |
    • {{ error }}
    • 35 | {% endfor %} 36 |
    37 | {% endif %} 38 |
    39 | 40 |
    41 | {{ form.use_trips.label }} 42 | {{ form.use_trips }} 43 | {% if form.use_trips.errors %} 44 |
      45 | {% for error in form.use_trips.errors %} 46 |
    • {{ error }}
    • 47 | {% endfor %} 48 |
    49 | {% endif %} 50 |
    51 | 52 |
    53 | {{ form.use_charges.label }} 54 | {{ form.use_charges }} 55 | {% if form.use_charges.errors %} 56 |
      57 | {% for error in form.use_charges.errors %} 58 |
    • {{ error }}
    • 59 | {% endfor %} 60 |
    61 | {% endif %} 62 |
    63 | 64 |
    65 | {{ form.use_refueling.label }} 66 | {{ form.use_refueling }} 67 | {% if form.use_refueling.errors %} 68 |
      69 | {% for error in form.use_refueling.errors %} 70 |
    • {{ error }}
    • 71 | {% endfor %} 72 |
    73 | {% endif %} 74 |
    75 | 76 |
    77 | {{ form.use_journey.label }} 78 | {{ form.use_journey }} 79 | {% if form.use_journey.errors %} 80 |
      81 | {% for error in form.use_journey.errors %} 82 |
    • {{ error }}
    • 83 | {% endfor %} 84 |
    85 | {% endif %} 86 |
    87 | 88 | {% if form.name.data %} 89 | {{ form.save }} 90 | {% else %} 91 | {{ form.add }} 92 | {% endif %} 93 | {{ form.delete }} 94 | 95 |
    96 |
    97 | {% endif %} 98 | {% endblock %} -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(-300)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 50 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(-300)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64444, 55 | "lon": 10.12253, 56 | "carCapturedTimestamp": "demodate(0)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel/002_0s_parking.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(0)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 51 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(0)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64444, 55 | "lon": 10.12253, 56 | "carCapturedTimestamp": "demodate(0)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_splitted/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(-300)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 50 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(-300)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64895, 55 | "lon": 10.16238, 56 | "carCapturedTimestamp": "demodate(-300)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /grafana/public/img/icons/car.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_splitted/002_0s_parking.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(0)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 51 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(0)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64895, 55 | "lon": 10.16238, 56 | "carCapturedTimestamp": "demodate(-300)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_position_before/001_0s.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(-300)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 50 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(-300)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64895, 55 | "lon": 10.16238, 56 | "carCapturedTimestamp": "demodate(-300)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /vwsfriend/tests/demos/hybrid_refuel_position_before/002_0s_parking.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/AAABBBCCCDDD12345?resolution=2x":[ 3 | { 4 | "data":[] 5 | }, 6 | "2021-10-19 06:24:15.072272" 7 | ], 8 | 9 | 10 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/selectivestatus?jobs=all": [ 11 | { 12 | "userCapabilities": { 13 | "capabilitiesStatus": { 14 | "value": [{ 15 | "id": "parkingPosition", 16 | "expirationDate": "2051-05-12T11:53:00Z", 17 | "userDisablingAllowed": false 18 | }] 19 | } 20 | }, 21 | "fuelStatus": { 22 | "rangeStatus": { 23 | "value": { 24 | "carCapturedTimestamp": "demodate(0)", 25 | "carType": "hybrid", 26 | "primaryEngine": { 27 | "type": "gasoline", 28 | "currentSOC_pct": 10, 29 | "remainingRange_km": 51 30 | }, 31 | "secondaryEngine": { 32 | "type": "electric", 33 | "currentSOC_pct": 64, 34 | "remainingRange_km": 35 35 | }, 36 | "totalRange_km": 85 37 | } 38 | } 39 | }, 40 | "measurements": { 41 | "odometerStatus": { 42 | "value": { 43 | "carCapturedTimestamp": "demodate(0)", 44 | "odometer": 4166 45 | } 46 | } 47 | } 48 | }, 49 | "now(+0)" 50 | ], 51 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles/AAABBBCCCDDD12345/parkingposition": [ 52 | { 53 | "data": { 54 | "lat": 53.64895, 55 | "lon": 10.16238, 56 | "carCapturedTimestamp": "demodate(-300)" 57 | } 58 | }, 59 | "now(+0)" 60 | ], 61 | "https://emea.bff.cariad.digital/vehicle/v1/vehicles": [ 62 | { 63 | "data": [ 64 | { 65 | "vin": "AAABBBCCCDDD12345", 66 | "role": "PRIMARY_USER", 67 | "enrollmentStatus": "COMPLETED", 68 | "model": "Demo Test Vehicle", 69 | "nickname": "Test Vehicle", 70 | "capabilities": [{ 71 | "id": "parkingPosition", 72 | "expirationDate": "2051-05-12T11:53:00Z", 73 | "userDisablingAllowed": false 74 | }], 75 | "images": {}, 76 | "coUsers": [] 77 | } 78 | ] 79 | }, 80 | "now(+0)" 81 | ] 82 | } -------------------------------------------------------------------------------- /.github/workflows/grafana-docker.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: grafana Build 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ main ] 10 | tags: 11 | - "v*" 12 | paths: 13 | - .github/workflows/grafana-docker.yml 14 | - grafana/** 15 | 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | vwsfriend-grafana: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Docker meta 25 | id: meta 26 | uses: docker/metadata-action@v5 27 | with: 28 | images: | 29 | tillsteinbach/vwsfriend-grafana 30 | ghcr.io/tillsteinbach/vwsfriend-grafana 31 | tags: | 32 | type=edge, 33 | type=pep440,pattern={{version}} 34 | - name: Version from pushed tag 35 | if: startsWith( github.ref, 'refs/tags/v' ) 36 | run: | 37 | # from refs/tags/v1.2.3 get 1.2.3 38 | echo "version=$(echo $GITHUB_REF | sed 's#.*/v##')" >> $GITHUB_ENV 39 | - name: Autobump version 40 | if: startsWith( github.ref, 'refs/tags/v' ) 41 | working-directory: grafana 42 | run: | 43 | PLACEHOLDER=" \"content\": \"VWsFriend version: \[0.0.0dev\](https://github.com/tillsteinbach/VWsFriend/)\"," 44 | REPLACEMENT=" \"content\": \"VWsFriend version: \[${{ env.version }}\](https://github.com/tillsteinbach/VWsFriend/releases/tag/v${{ env.version }})\"," 45 | VERSION_FILE="dashboards/vwsfriend/VWsFriend/overview.json" 46 | # ensure the placeholder is there. If grep doesn't find the placeholder 47 | # it exits with exit code 1 and github actions aborts the build. 48 | grep "$PLACEHOLDER" "$VERSION_FILE" 49 | sed -i "s|$PLACEHOLDER|$REPLACEMENT|g" "$VERSION_FILE" 50 | grep "$REPLACEMENT" "$VERSION_FILE" 51 | - name: Set up QEMU 52 | uses: docker/setup-qemu-action@v3 53 | - name: Setup Docker Buildx 54 | uses: docker/setup-buildx-action@v3.6.1 55 | - name: Login to DockerHub 56 | uses: docker/login-action@v3.3.0 57 | with: 58 | username: ${{ secrets.DOCKERHUB_USERNAME }} 59 | password: ${{ secrets.DOCKERHUB_TOKEN }} 60 | - name: Login to GitHub Container Registry 61 | uses: docker/login-action@v3.3.0 62 | with: 63 | registry: ghcr.io 64 | username: ${{ github.repository_owner }} 65 | password: ${{ secrets.GITHUB_TOKEN }} 66 | - name: Build and push 67 | id: docker_build 68 | uses: docker/build-push-action@v6.7.0 69 | with: 70 | context: grafana 71 | push: ${{ github.event_name != 'pull_request' }} 72 | platforms: linux/amd64,linux/arm/v7,linux/arm64 73 | tags: ${{ steps.meta.outputs.tags }} 74 | labels: ${{ steps.meta.outputs.labels }} 75 | cache-from: type=gha 76 | cache-to: type=gha,mode=max 77 | --------------------------------------------------------------------------------