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 |
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 |
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 |
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 |
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 |
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 |
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 |
11 | VWsFriend
12 |
13 | {% if current_app.connector.withDB and g.dbsettings is not none %}
14 | Visualization
15 | {% endif %}
16 | Status
17 | {% if current_app.connector.withDB %}
18 | Database
19 | {% endif %}
20 | {% if current_app.homekitDriver %}
21 | Homekit
22 | {% endif %}
23 | {% if current_app.connector.withDB and current_app.config['SQLALCHEMY_DATABASE_URI'].startswith('postgresql://') %}
24 | Backup
25 | {% endif %}
26 | {% if current_user.is_authenticated %}
27 | Logout
28 | {% else %}
29 | Login
30 | {% endif %}
31 |
32 |
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 |
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 |
14 |
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 |
29 |
30 |
31 | {% endfor %}
32 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------