diff --git a/_ci/apikeys-ci.xml b/_ci/apikeys-ci.xml index a2b0289cb..9da033c4c 100644 --- a/_ci/apikeys-ci.xml +++ b/_ci/apikeys-ci.xml @@ -8,4 +8,5 @@ ci ci ci:ci + ci diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 94c7857e9..7a0533ab5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -129,6 +129,17 @@ android { // add API keys from environment variable if not set in apikeys.xml applicationVariants.all { + var evmapKey = + System.getenv("EVMAP_API_KEY") ?: project.findProperty("EVMAP_API_KEY")?.toString() + if (evmapKey == null && project.hasProperty("EVMAP_API_KEY_ENCRYPTED")) { + evmapKey = decode( + project.findProperty("EVMAP_API_KEY_ENCRYPTED").toString(), + "FmK.d,-f*p+rD+WK!eds" + ) + } + if (evmapKey != null) { + resValue("string", "evmap_key", evmapKey) + } val goingelectricKey = System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY") ?.toString() diff --git a/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json b/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json new file mode 100644 index 000000000..49659ad77 --- /dev/null +++ b/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json @@ -0,0 +1,938 @@ +{ + "formatVersion": 1, + "database": { + "version": 28, + "identityHash": "84f71cce385c444726ba336834ddf6b4", + "entities": [ + { + "tableName": "ChargeLocation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "coordinates", + "columnName": "coordinates", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "chargepoints", + "columnName": "chargepoints", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "network", + "columnName": "network", + "affinity": "TEXT" + }, + { + "fieldPath": "dataSourceUrl", + "columnName": "dataSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "editUrl", + "columnName": "editUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "barrierFree", + "columnName": "barrierFree", + "affinity": "INTEGER" + }, + { + "fieldPath": "operator", + "columnName": "operator", + "affinity": "TEXT" + }, + { + "fieldPath": "generalInformation", + "columnName": "generalInformation", + "affinity": "TEXT" + }, + { + "fieldPath": "amenities", + "columnName": "amenities", + "affinity": "TEXT" + }, + { + "fieldPath": "locationDescription", + "columnName": "locationDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "photos", + "columnName": "photos", + "affinity": "TEXT" + }, + { + "fieldPath": "chargecards", + "columnName": "chargecards", + "affinity": "TEXT" + }, + { + "fieldPath": "accessibility", + "columnName": "accessibility", + "affinity": "TEXT" + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT" + }, + { + "fieldPath": "networkUrl", + "columnName": "networkUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "chargerUrl", + "columnName": "chargerUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "timeRetrieved", + "columnName": "timeRetrieved", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDetailed", + "columnName": "isDetailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coordinatesProjected", + "columnName": "coordinatesProjected", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "address.city", + "columnName": "city", + "affinity": "TEXT" + }, + { + "fieldPath": "address.country", + "columnName": "country", + "affinity": "TEXT" + }, + { + "fieldPath": "address.postcode", + "columnName": "postcode", + "affinity": "TEXT" + }, + { + "fieldPath": "address.street", + "columnName": "street", + "affinity": "TEXT" + }, + { + "fieldPath": "faultReport.created", + "columnName": "fault_report_created", + "affinity": "INTEGER" + }, + { + "fieldPath": "faultReport.description", + "columnName": "fault_report_description", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.twentyfourSeven", + "columnName": "twentyfourSeven", + "affinity": "INTEGER" + }, + { + "fieldPath": "openinghours.description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.monday.start", + "columnName": "mostart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.monday.end", + "columnName": "moend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.tuesday.start", + "columnName": "tustart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.tuesday.end", + "columnName": "tuend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.wednesday.start", + "columnName": "westart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.wednesday.end", + "columnName": "weend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.thursday.start", + "columnName": "thstart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.thursday.end", + "columnName": "thend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.friday.start", + "columnName": "frstart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.friday.end", + "columnName": "frend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.saturday.start", + "columnName": "sastart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.saturday.end", + "columnName": "saend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.sunday.start", + "columnName": "sustart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.sunday.end", + "columnName": "suend", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.holiday.start", + "columnName": "hostart", + "affinity": "TEXT" + }, + { + "fieldPath": "openinghours.days.holiday.end", + "columnName": "hoend", + "affinity": "TEXT" + }, + { + "fieldPath": "cost.freecharging", + "columnName": "freecharging", + "affinity": "INTEGER" + }, + { + "fieldPath": "cost.freeparking", + "columnName": "freeparking", + "affinity": "INTEGER" + }, + { + "fieldPath": "cost.descriptionShort", + "columnName": "descriptionShort", + "affinity": "TEXT" + }, + { + "fieldPath": "cost.descriptionLong", + "columnName": "descriptionLong", + "affinity": "TEXT" + }, + { + "fieldPath": "chargepriceData.country", + "columnName": "chargepricecountry", + "affinity": "TEXT" + }, + { + "fieldPath": "chargepriceData.network", + "columnName": "chargepricenetwork", + "affinity": "TEXT" + }, + { + "fieldPath": "chargepriceData.plugTypes", + "columnName": "chargepriceplugTypes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "dataSource" + ] + } + }, + { + "tableName": "Favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "favoriteId", + "columnName": "favoriteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chargerId", + "columnName": "chargerId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chargerDataSource", + "columnName": "chargerDataSource", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "favoriteId" + ] + }, + "indices": [ + { + "name": "index_Favorite_chargerId_chargerDataSource", + "unique": false, + "columnNames": [ + "chargerId", + "chargerDataSource" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)" + } + ], + "foreignKeys": [ + { + "table": "ChargeLocation", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "chargerId", + "chargerDataSource" + ], + "referencedColumns": [ + "id", + "dataSource" + ] + } + ] + }, + { + "tableName": "BooleanFilterValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profile", + "columnName": "profile", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key", + "profile", + "dataSource" + ] + }, + "indices": [ + { + "name": "index_BooleanFilterValue_profile_dataSource", + "unique": false, + "columnNames": [ + "profile", + "dataSource" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)" + } + ], + "foreignKeys": [ + { + "table": "FilterProfile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profile", + "dataSource" + ], + "referencedColumns": [ + "id", + "dataSource" + ] + } + ] + }, + { + "tableName": "MultipleChoiceFilterValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "all", + "columnName": "all", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profile", + "columnName": "profile", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key", + "profile", + "dataSource" + ] + }, + "indices": [ + { + "name": "index_MultipleChoiceFilterValue_profile_dataSource", + "unique": false, + "columnNames": [ + "profile", + "dataSource" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)" + } + ], + "foreignKeys": [ + { + "table": "FilterProfile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profile", + "dataSource" + ], + "referencedColumns": [ + "id", + "dataSource" + ] + } + ] + }, + { + "tableName": "SliderFilterValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profile", + "columnName": "profile", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key", + "profile", + "dataSource" + ] + }, + "indices": [ + { + "name": "index_SliderFilterValue_profile_dataSource", + "unique": false, + "columnNames": [ + "profile", + "dataSource" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)" + } + ], + "foreignKeys": [ + { + "table": "FilterProfile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profile", + "dataSource" + ], + "referencedColumns": [ + "id", + "dataSource" + ] + } + ] + }, + { + "tableName": "FilterProfile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "dataSource", + "id" + ] + }, + "indices": [ + { + "name": "index_FilterProfile_dataSource_name", + "unique": true, + "columnNames": [ + "dataSource", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)" + } + ] + }, + { + "tableName": "RecentAutocompletePlace", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryText", + "columnName": "primaryText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondaryText", + "columnName": "secondaryText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latLng", + "columnName": "latLng", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewport", + "columnName": "viewport", + "affinity": "TEXT" + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "dataSource" + ] + } + }, + { + "tableName": "GEPlug", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + } + }, + { + "tableName": "GENetwork", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + } + }, + { + "tableName": "GEChargeCard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "OCMConnectionType", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formalName", + "columnName": "formalName", + "affinity": "TEXT" + }, + { + "fieldPath": "discontinued", + "columnName": "discontinued", + "affinity": "INTEGER" + }, + { + "fieldPath": "obsolete", + "columnName": "obsolete", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "OCMCountry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isoCode", + "columnName": "isoCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "continentCode", + "columnName": "continentCode", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "OCMOperator", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "websiteUrl", + "columnName": "websiteUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contactEmail", + "columnName": "contactEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "contactTelephone1", + "columnName": "contactTelephone1", + "affinity": "TEXT" + }, + { + "fieldPath": "contactTelephone2", + "columnName": "contactTelephone2", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "OSMNetwork", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + } + }, + { + "tableName": "SavedRegion", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "region", + "columnName": "region", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "dataSource", + "columnName": "dataSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeRetrieved", + "columnName": "timeRetrieved", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filters", + "columnName": "filters", + "affinity": "TEXT" + }, + { + "fieldPath": "isDetailed", + "columnName": "isDetailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SavedRegion_filters_dataSource", + "unique": false, + "columnNames": [ + "filters", + "dataSource" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt index 021dd56d7..fa7bd3b3c 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt @@ -175,6 +175,7 @@ class AvailabilityRepository(context: Context) { RheinenergieAvailabilityDetector(okhttp), teslaOwnerAvailabilityDetector, TeslaGuestAvailabilityDetector(okhttp), + NobilAvailabilityDetector(okhttp, context), EnBwAvailabilityDetector(okhttp), NewMotionAvailabilityDetector(okhttp) ) diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt new file mode 100644 index 000000000..c8d3794e7 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt @@ -0,0 +1,109 @@ +package net.vonforst.evmap.api.availability + +import android.content.Context +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson +import net.vonforst.evmap.R +import net.vonforst.evmap.model.ChargeLocation +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path +import java.time.Instant + +internal class InstantStringAdapter { + @FromJson + fun fromJson(value: String?): Instant? = value?.let { + Instant.parse(value) + } + + @ToJson + fun toJson(value: Instant?): String? = value?.toString() +} + +interface NobilRealtimeApi { + @GET("{nobilId}") + suspend fun getAvailability( + @Path("nobilId") nobilId: String, + @Header("X-Api-Key") apiKey: String + ): List + + companion object { + fun create(client: OkHttpClient): NobilRealtimeApi { + val retrofit = Retrofit.Builder() + .baseUrl("https://api.ev-map.app/nobil/api/realtime/") + .addConverterFactory( + MoshiConverterFactory.create( + Moshi.Builder().add(InstantStringAdapter()).build() + ) + ) + .client(client) + .build() + return retrofit.create(NobilRealtimeApi::class.java) + } + } +} + +@JsonClass(generateAdapter = true) +data class NobilChargepointState( + val evseUid: String, + val status: String, + val timestamp: Instant +) + +class NobilAvailabilityDetector(client: OkHttpClient, context: Context) : + BaseAvailabilityDetector(client) { + val api = NobilRealtimeApi.create(client) + val apiKey = context.getString(R.string.evmap_key) + + override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus { + val nobilId = when (location.address?.country) { + "Norway" -> "NOR" + "Sweden" -> "SWE" + else -> throw AvailabilityDetectorException("nobil: unsupported country") + } + "_%05d".format(location.id) + + val availability = api.getAvailability(nobilId, apiKey) + if (availability.isEmpty()) { + throw AvailabilityDetectorException("nobil: no real-time data available") + } + return ChargeLocationStatus( + location.chargepointsMerged.associateWith { cp -> + cp.evseUIds!!.map { evseUId -> + when (availability.find { it.evseUid == evseUId }?.status) { + "AVAILABLE" -> ChargepointStatus.AVAILABLE + "BLOCKED" -> ChargepointStatus.OCCUPIED + "CHARGING" -> ChargepointStatus.CHARGING + "INOPERATIVE" -> ChargepointStatus.FAULTED + "OUTOFORDER" -> ChargepointStatus.FAULTED + "PLANNED" -> ChargepointStatus.FAULTED + "REMOVED" -> ChargepointStatus.FAULTED + "RESERVED" -> ChargepointStatus.OCCUPIED + "UNKNOWN" -> ChargepointStatus.UNKNOWN + else -> ChargepointStatus.UNKNOWN + } + } + }, + "Nobil", + location.chargepointsMerged.associateWith { cp -> + if (cp.evseIds != null) cp.evseIds.map { it ?: "??" } else listOf() + }, + lastChange = location.chargepointsMerged.associateWith { cp -> + cp.evseUIds!!.map { evseUId -> + availability.find { it.evseUid == evseUId }?.timestamp + } + } + ) + } + + override fun isChargerSupported(charger: ChargeLocation): Boolean { + return when (charger.dataSource) { + "nobil" -> charger.chargepoints.any { it.evseUIds?.isNotEmpty() == true } + else -> false + } + } +} diff --git a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt index 7fd2b2517..ff9ba8d2d 100644 --- a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt @@ -279,9 +279,10 @@ data class NobilChargerStation( val connectionVoltage = if (attribs["12"]?.attrVal is String) attribs["12"]?.attrVal.toString().toDoubleOrNull() else null val connectionCurrent = if (attribs["31"]?.attrVal is String) attribs["31"]?.attrVal.toString().toDoubleOrNull() else null + val evseUId = if (attribs["27"]?.attrVal is String) listOf(attribs["27"]?.attrVal.toString()) else null val evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null - return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId) + return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId, evseUId) } } } diff --git a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt index 322fc013f..99851ad93 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -140,10 +140,12 @@ data class ChargeLocation( .filter { it.type == variant.type && it.power == variant.power } val count = filtered.sumOf { it.count } val mergedEvseIds = filtered.map { if (it.evseIds == null) List(it.count) {null} else it.evseIds }.flatten() + val mergedEvseUIds = filtered.map { if (it.evseUIds == null) List(it.count) {null} else it.evseUIds }.flatten() Chargepoint(variant.type, variant.power, count, filtered.map { it.current }.distinct().singleOrNull(), filtered.map { it.voltage }.distinct().singleOrNull(), - if (mergedEvseIds.all { it == null }) null else mergedEvseIds + if (mergedEvseIds.all { it == null }) null else mergedEvseIds, + if (mergedEvseUIds.all { it == null }) null else mergedEvseUIds ) } } @@ -425,7 +427,9 @@ data class Chargepoint( // (each of the three can be separately limited) val voltage: Double? = null, // Electric Vehicle Supply Equipment Ids for this Chargepoint's plugs/sockets - val evseIds: List? = null + val evseIds: List? = null, + // Electric Vehicle Supply Equipment Unique Ids for this Chargepoint's plugs/sockets + val evseUIds: List? = null ) : Equatable, Parcelable { fun hasKnownPower(): Boolean = power != null fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null diff --git a/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt b/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt index cfd0f34f5..3a443e600 100644 --- a/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt @@ -25,4 +25,4 @@ data class Favorite( data class FavoriteWithDetail( @Embedded val favorite: Favorite, @Embedded val charger: ChargeLocation -) +) \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt index 32b4c4c51..662684d1f 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -40,7 +40,7 @@ import net.vonforst.evmap.model.SliderFilterValue OCMOperator::class, OSMNetwork::class, SavedRegion::class - ], version = 27 + ], version = 28 ) @TypeConverters(Converters::class, GeometryConverters::class) abstract class AppDatabase : RoomDatabase() { @@ -85,7 +85,7 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16, MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21, MIGRATION_22, MIGRATION_23, MIGRATION_24, MIGRATION_25, MIGRATION_26, - MIGRATION_27 + MIGRATION_27, MIGRATION_28 ) .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { @@ -547,6 +547,14 @@ abstract class AppDatabase : RoomDatabase() { db.execSQL("ALTER TABLE `ChargeLocation` ADD `accessibility` TEXT") } } + + private val MIGRATION_28 = object : Migration(27, 28) { + override fun migrate(db: SupportSQLiteDatabase) { + // Force nobil data refresh to fetch EVSE UId attributes needed for real-time data + db.execSQL("DELETE FROM SavedRegion WHERE `dataSource` = 'nobil'") + db.execSQL("DELETE FROM ChargeLocation WHERE `dataSource` = 'nobil'") + } + } } /** diff --git a/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt b/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt index 309d2f8fd..28eb8e888 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt @@ -13,10 +13,10 @@ interface FavoritesDao { @Delete suspend fun delete(vararg favorites: Favorite) - @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id") + @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL") fun getAllFavorites(): LiveData> - @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id") + @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL") suspend fun getAllFavoritesAsync(): List @SkipQueryVerification diff --git a/doc/api_keys.md b/doc/api_keys.md index 4f9da8c49..a36f7bd69 100644 --- a/doc/api_keys.md +++ b/doc/api_keys.md @@ -38,6 +38,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under insert your nobil key here + + insert your EVMap key here + ``` @@ -236,6 +239,13 @@ key and documentation. If you don't want to test this functionality, simply leave the API key blank. +### EVMap + +EVMap provides APIs to fetch Nobil real-time data. + +Contact [EVMap](mailto:evmap@vonforst.net) to get an API key. + + Crash reporting ---------------