├── .ci_cd ├── env │ ├── .gitignore │ └── .env ├── host_init │ └── .gitignore └── README.md ├── ui ├── .gitignore ├── component │ ├── rest │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── tsconfig.json │ │ ├── webpack.config.js │ │ ├── src │ │ │ └── index.ts │ │ ├── package.json │ │ └── build.gradle │ └── model │ │ ├── .gitignore │ │ ├── src │ │ ├── index.ts │ │ └── util.ts │ │ ├── .npmignore │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── build.gradle ├── app │ ├── custom │ │ ├── .babelrc │ │ ├── locales │ │ │ ├── nl │ │ │ │ └── app.json │ │ │ └── en │ │ │ │ └── app.json │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ ├── build.gradle │ │ ├── babel.config.js │ │ ├── webpack.config.js │ │ ├── src │ │ │ ├── pages │ │ │ │ └── page-custom.ts │ │ │ └── index.ts │ │ ├── package.json │ │ └── index.html │ └── README.md ├── .prettierrc.json ├── .eslintrc.json ├── tsconfig.json ├── typedoc.js └── build.gradle ├── deployment ├── manager │ ├── provisioning │ │ └── .gitignore │ ├── consoleappconfig │ │ └── .gitignore │ ├── app │ │ ├── images │ │ │ ├── logo.png │ │ │ ├── favicon.ico │ │ │ └── logo-mobile.png │ │ └── manager_config.json │ └── README.md ├── Dockerfile ├── map │ └── README.md ├── keycloak │ └── themes │ │ └── README.md └── build.gradle ├── .env ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── manager ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.openremote.manager.mqtt.MQTTHandler │ │ └── java │ │ └── telematics │ │ └── teltonika │ │ ├── TeltonikaParameterData.java │ │ ├── ITeltonikaPayload.java │ │ ├── TeltonikaPayloadFactory.java │ │ ├── TeltonikaResponsePayload.java │ │ ├── helpers │ │ └── TeltonikaConfigurationFactory.java │ │ └── TeltonikaConfiguration.java └── build.gradle ├── setup ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.openremote.model.setup.SetupTasks │ │ └── java │ │ └── org │ │ └── openremote │ │ └── manager │ │ └── setup │ │ └── custom │ │ ├── CustomManagerSetup.java │ │ ├── CustomKeycloakSetup.java │ │ └── CustomSetupTasks.java └── build.gradle ├── model ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.openremote.model.AssetModelProvider │ │ └── java │ │ └── org │ │ └── openremote │ │ └── model │ │ ├── teltonika │ │ ├── TeltonikaDataPayloadModel.java │ │ ├── State.java │ │ ├── IMEIValidator.java │ │ ├── PayloadJsonObject.java │ │ ├── TeltonikaConfigurationAsset.java │ │ ├── TeltonikaModelConfigurationAsset.java │ │ └── TeltonikaParameter.java │ │ └── custom │ │ ├── CustomValueTypes.java │ │ ├── CustomData.java │ │ ├── CustomEndpointResource.java │ │ ├── CustomAssetModelProvider.java │ │ ├── VehicleAsset.java │ │ ├── AssetStateDuration.java │ │ ├── CustomAsset.java │ │ └── CarAsset.java └── build.gradle ├── .yarnrc.yml ├── .gitattributes ├── .editorconfig ├── package.json ├── agent ├── build.gradle └── src │ └── main │ └── java │ └── org │ └── openremote │ └── agent │ └── teltonika │ └── CustomAgentModelProvider.java ├── gradle.properties ├── settings.gradle ├── .gitignore ├── test ├── src │ └── test │ │ └── resources │ │ └── teltonika │ │ └── teltonikaValidPayload1.json └── build.gradle ├── profile ├── dev-testing.yml ├── dev-ui.yml ├── dev-proxy.yml └── prod_cicd.yml ├── .github └── workflows │ ├── ci_cd.yml │ └── publish-release.yml ├── model-util ├── build.gradle └── src │ └── main │ └── java │ ├── AggregatedApiClient.java │ ├── CustomAggregatedApiClient.java │ ├── CustomTypeProcessor.java │ ├── JsonSerializeExtension.java │ ├── CustomExtension.java │ └── org │ └── openremote │ └── model │ └── util │ └── AssetModelInfoExtension.java ├── gradlew.bat ├── README.md ├── docker-compose-test.yml ├── docker-compose.yml ├── gradlew └── project.gradle /.ci_cd/env/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ci_cd/host_init/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /deployment/manager/provisioning/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deployment/manager/consoleappconfig/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/component/rest/.gitignore: -------------------------------------------------------------------------------- 1 | src/restclient.ts 2 | -------------------------------------------------------------------------------- /.ci_cd/env/.env: -------------------------------------------------------------------------------- 1 | ENV_COMPOSE_FILE=profile/prod_cicd.yml 2 | -------------------------------------------------------------------------------- /ui/app/custom/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /ui/app/custom/locales/nl/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "customPage": "Aangepaste pagina" 3 | } 4 | -------------------------------------------------------------------------------- /ui/component/model/.gitignore: -------------------------------------------------------------------------------- 1 | src/model.ts 2 | src/typescript-generator-info.json 3 | -------------------------------------------------------------------------------- /ui/app/README.md: -------------------------------------------------------------------------------- 1 | Custom UI apps can be added here use the manager UI app as a template. 2 | -------------------------------------------------------------------------------- /ui/component/model/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./model"; 2 | export {AssetModelUtil} from "./util"; 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | OR_HOSTNAME=localhost 2 | OR_SSL_PORT=443 3 | KC_HOSTNAME_PORT=-1 4 | WEBSERVER_LISTEN_HOST=0.0.0.0 5 | -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 120, 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /ui/app/custom/locales/en/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "customPage": "Custom page", 3 | "customString": "My custom string" 4 | } 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openremote/fleet-management/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /manager/src/main/resources/META-INF/services/org.openremote.manager.mqtt.MQTTHandler: -------------------------------------------------------------------------------- 1 | telematics.teltonika.TeltonikaMQTTHandler 2 | -------------------------------------------------------------------------------- /setup/src/main/resources/META-INF/services/org.openremote.model.setup.SetupTasks: -------------------------------------------------------------------------------- 1 | org.openremote.manager.setup.custom.CustomSetupTasks 2 | -------------------------------------------------------------------------------- /deployment/manager/app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openremote/fleet-management/HEAD/deployment/manager/app/images/logo.png -------------------------------------------------------------------------------- /model/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider: -------------------------------------------------------------------------------- 1 | org.openremote.model.custom.CustomAssetModelProvider 2 | -------------------------------------------------------------------------------- /deployment/manager/app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openremote/fleet-management/HEAD/deployment/manager/app/images/favicon.ico -------------------------------------------------------------------------------- /ui/component/model/.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | build.gradle 3 | test/ 4 | src/ 5 | webpack.config.js 6 | tsconfig.json 7 | dist/*.map 8 | .tsbuildinfo 9 | -------------------------------------------------------------------------------- /ui/component/rest/.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | build.gradle 3 | test/ 4 | src/ 5 | webpack.config.js 6 | tsconfig.json 7 | dist/*.map 8 | .tsbuildinfo 9 | -------------------------------------------------------------------------------- /deployment/manager/app/images/logo-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openremote/fleet-management/HEAD/deployment/manager/app/images/logo-mobile.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.1.0.cjs 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.ci_cd/README.md: -------------------------------------------------------------------------------- 1 | # CI/CD workflow files 2 | This directory follows the same structure as the main [OpenRemote repo](https://github.com/openremote/openremote/tree/master/.ci_cd). 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "yarn@4.1.0", 3 | "private": true, 4 | "workspaces": [ 5 | "openremote", 6 | "ui/app/*", 7 | "ui/component/*", 8 | "ui/demo/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /model/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java-library" 2 | 3 | dependencies { 4 | api "io.openremote:openremote-model:$openremoteVersion" 5 | } 6 | 7 | tasks.register('installDist') { 8 | dependsOn jar 9 | } 10 | -------------------------------------------------------------------------------- /agent/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java-library" 2 | 3 | dependencies { 4 | api project(":model") 5 | api "io.openremote:openremote-agent:$openremoteVersion" 6 | } 7 | 8 | tasks.register('installDist') { 9 | dependsOn jar 10 | } 11 | -------------------------------------------------------------------------------- /ui/component/model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "src", 6 | "preserveConstEnums": true 7 | }, 8 | "include": [ 9 | "./src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /ui/app/custom/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | test/integration/screenshots-current/ 4 | _site/ 5 | .DS_Store 6 | src/**/*.d.ts 7 | src/**/*.js 8 | src/**/*.js.map 9 | openremote/**/*.d.ts 10 | openremote/**/*.js 11 | openremote/**/*.js.map 12 | -------------------------------------------------------------------------------- /ui/component/rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "./src" 9 | ], 10 | "references": [ 11 | { "path": "../model" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | 3 | projectName = fleet-management 4 | version = 1.0-SNAPSHOT 5 | 6 | openremoteVersion = 1.8.1 7 | 8 | jacksonVersion = 2.16.0 9 | typescriptGeneratorVersion = 3.2.1263 10 | 11 | 12 | swaggerVersion=2.2.28 13 | swaggeruiVersion = 5.18.2 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /manager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java-library" 2 | 3 | dependencies { 4 | api project(":agent") 5 | api project(":model") 6 | api "io.openremote:openremote-container:$openremoteVersion" 7 | api "io.openremote:openremote-manager:$openremoteVersion" 8 | } 9 | 10 | tasks.register('installDist') { 11 | dependsOn jar 12 | } 13 | -------------------------------------------------------------------------------- /ui/app/custom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "src", 6 | "strict": false 7 | }, 8 | "include": [ 9 | "./src" 10 | ], 11 | "references": [ 12 | { "path": "../../component/model" }, 13 | { "path": "../../component/rest" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui/app/custom/build.gradle: -------------------------------------------------------------------------------- 1 | buildDir = "dist" 2 | 3 | tasks.register('clean') { 4 | dependsOn npmClean 5 | } 6 | 7 | tasks.register('installDist', Copy) { 8 | dependsOn npmBuild 9 | mustRunAfter(resolveTask(":manager:installDist")) 10 | from project.buildDir 11 | into "${project(':deployment').buildDir}/image/manager/app/${projectDir.name}" 12 | } 13 | -------------------------------------------------------------------------------- /deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------------------- 2 | # Docker image for populating deployment-data volume with project specific customisations 3 | # ---------------------------------------------------------------------------------------- 4 | FROM alpine:latest 5 | 6 | RUN mkdir -p /deployment/manager/extensions 7 | ADD image /deployment 8 | -------------------------------------------------------------------------------- /ui/component/rest/webpack.config.js: -------------------------------------------------------------------------------- 1 | const util = require("@openremote/util"); 2 | 3 | bundles = { 4 | "index": { 5 | vendor: { 6 | "axios": "axios", 7 | "qs": "Qs" 8 | }, 9 | excludeOr: true 10 | }, 11 | "index.bundle": { 12 | excludeOr: true, 13 | } 14 | }; 15 | 16 | module.exports = util.generateExports(__dirname); 17 | -------------------------------------------------------------------------------- /ui/component/rest/src/index.ts: -------------------------------------------------------------------------------- 1 | import {ApiClient} from "./restclient"; 2 | 3 | 4 | 5 | export class RestApi { 6 | 7 | protected _apiClient: ApiClient; 8 | 9 | constructor(apiClient: ApiClient) { 10 | this._apiClient = apiClient; 11 | } 12 | 13 | get api() { 14 | return this._apiClient; 15 | } 16 | } 17 | 18 | export default new RestApi(new ApiClient()); 19 | -------------------------------------------------------------------------------- /deployment/map/README.md: -------------------------------------------------------------------------------- 1 | ## Map customisation 2 | As well as the below information please see the [Manager endpoints and file paths](https://github.com/openremote/openremote/wiki/Architecture:-Manager-endpoints-and-file-paths) wiki. 3 | 4 | The default endpoints and file paths look in the `/deployment/map`, see [here](https://github.com/openremote/openremote/wiki/User-Guide%3A-Custom-deployment#customising-the-map) for more details. 5 | -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "tsconfig.json", 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "standard", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier" 12 | ], 13 | "plugins": ["@typescript-eslint"], 14 | "env": { 15 | "es6": true 16 | }, 17 | "ignorePatterns": ["dist", "lib", "node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /deployment/manager/app/manager_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loadLocales": true, 3 | "realms": { 4 | "default": { 5 | "appTitle": "Custom Project", 6 | "styles": ":host > * {--or-app-color2: #F9F9F9; --or-app-color3: #22211f; --or-app-color4: #0c4da2; --or-app-color5: #CCCCCC;}", 7 | "logo": "/images/logo.png", 8 | "logoMobile": "/images/logo-mobile.png", 9 | "favicon": "/images/favicon.ico", 10 | "language": "en" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "esNext", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "dom", 9 | "es2017", 10 | "es2019" 11 | ], 12 | "strict": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "experimentalDecorators": true, 16 | "allowSyntheticDefaultImports": true, 17 | "composite": true, 18 | "useDefineForClassFields": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /setup/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java-library" 2 | 3 | dependencies { 4 | api project(":model") 5 | api project(":manager") 6 | 7 | implementation "io.openremote.ui:openremote-insights:$openremoteVersion" 8 | implementation "io.openremote.ui:openremote-manager:$openremoteVersion" 9 | implementation "io.openremote.ui:openremote-shared:$openremoteVersion" 10 | implementation "io.openremote.ui:openremote-swagger:$openremoteVersion" 11 | } 12 | 13 | tasks.register('installDist') { 14 | dependsOn jar 15 | } 16 | -------------------------------------------------------------------------------- /ui/typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "mode": "modules", 3 | "out": "docs", 4 | "exclude": "test", 5 | "theme": "minimal", 6 | "ignoreCompilerErrors": true, 7 | "excludePrivate": true, 8 | "excludeProtected": true, 9 | "excludeNotExported": true, 10 | "target": "es6", 11 | "moduleResolution": "node", 12 | "preserveConstEnums": true, 13 | "stripInternal": true, 14 | "suppressExcessPropertyErrors": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "module": "esNext", 17 | "external-modulemap": ".*node_modules\/(@openremote\/[^\/]+)\/.*" 18 | } 19 | -------------------------------------------------------------------------------- /deployment/keycloak/themes/README.md: -------------------------------------------------------------------------------- 1 | ## Custom Keycloak themes 2 | Add a directory for each custom theme (the directory name is the theme name) and add the required theme templates within; you can use the default [openremote theme](https://github.com/openremote/keycloak/tree/main/themes/openremote) as a template alternatively refer to the keycloak themes documentation. 3 | 4 | This themes directory can then be volume mapped into the keycloak container at `/deployment/keycloak/themes`. 5 | 6 | ### Development 7 | To be able to see the custom theme in development (i.e. when using `dev-testing.yml` compose profile) you need to create a copy of `openremote/profile/dev-testing.yml` in the custom project `profile` directory. 8 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/TeltonikaDataPayloadModel.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import java.io.Serializable; 8 | 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | @JsonPropertyOrder({ 11 | "payload" 12 | }) 13 | public class TeltonikaDataPayloadModel implements Serializable { 14 | 15 | @JsonProperty("state") 16 | public State state; 17 | 18 | public TeltonikaDataPayloadModel() { 19 | } 20 | 21 | public TeltonikaDataPayloadModel(State state) { 22 | this.state = state; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ui/component/model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "model", 3 | "version": "1.0.0", 4 | "description": "Model Types", 5 | "main": "dist/umd/index.bundle.js", 6 | "module": "lib/index.js", 7 | "private": true, 8 | "exports": { 9 | ".": "./lib/index.js", 10 | "./*": "./lib/*.js" 11 | }, 12 | "types": "lib/index.d.ts", 13 | "files": [ 14 | "lib", 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "npx tsc -b --force", 19 | "test": "echo \"No tests\" && exit 0", 20 | "prepublishOnly": "npx tsc -b" 21 | }, 22 | "author": "OpenRemote", 23 | "license": "AGPL-3.0-or-later", 24 | "devDependencies": { 25 | "@openremote/util": "^1.3.4" 26 | }, 27 | "publishConfig": { 28 | "access": "restricted" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.cherryperry.gradle-file-encrypt" version "2.0.3" apply false 3 | id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.3" apply false 4 | id 'cz.habarta.typescript-generator' version "$typescriptGeneratorVersion" apply false 5 | id 'pl.allegro.tech.build.axion-release' version '1.18.16' apply false 6 | } 7 | 8 | rootProject.name = "$projectName" 9 | 10 | // Include sub-projects dynamically, every directory with a build.gradle (and no .buildignore) 11 | fileTree(dir: rootDir, include: "**/build.gradle", excludes: ["**/node_modules/**", "**/generic_app/**"]) 12 | .filter { it.parent != rootDir } 13 | .filter { !file("${it.parent}/.buildignore").exists() } 14 | .each { 15 | include it.parent.replace(rootDir.canonicalPath, "").replace("\\", ":").replace("/", ":") 16 | } 17 | -------------------------------------------------------------------------------- /ui/build.gradle: -------------------------------------------------------------------------------- 1 | afterEvaluate { 2 | // Add dependencies on model and rest typescript generation 3 | rootProject.getTasksByName('prepareUi', true).forEach { 4 | it.dependsOn resolveTask(":ui:component:model:generateTypeScript"), resolveTask(":ui:component:rest:generateTypeScript") 5 | } 6 | rootProject.getTasksByName('publishUi', true).forEach { 7 | it.dependsOn resolveTask(":ui:component:model:generateTypeScript"), resolveTask(":ui:component:rest:generateTypeScript") 8 | } 9 | rootProject.getTasksByName('npmBuild', true).forEach { 10 | it.dependsOn resolveTask(":ui:component:model:generateTypeScript"), resolveTask(":ui:component:rest:generateTypeScript") 11 | } 12 | } 13 | 14 | tasks.register('modelWatch') { 15 | dependsOn resolveTask(":ui:component:model:build"), resolveTask(":ui:component:rest:build") 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | [Oo]bj/ 5 | [Bb]in/ 6 | [Bb]uild/ 7 | [Oo]ut/ 8 | [Tt]mp/ 9 | 10 | # Other files and folders 11 | .settings/ 12 | .gradle/ 13 | .vagrant/ 14 | .node/ 15 | .classpath/ 16 | .classpath 17 | .project/ 18 | .project 19 | .vscode/ 20 | .idea/ 21 | .sts4-cache/ 22 | .local/ 23 | .DS_*/ 24 | apminsight-javaagent/ 25 | console/iOS/DerivedData 26 | console/iOS/Pods 27 | manager/.factorypath/ 28 | Pods/ 29 | node_modules/ 30 | .pnp.* 31 | .yarn/* 32 | !.yarn/patches 33 | !.yarn/plugins 34 | !.yarn/releases 35 | !.yarn/sdks 36 | !.yarn/versions 37 | 38 | # Specific files and logs 39 | openremote.log 40 | dive.log 41 | yarn-error.log 42 | local.properties 43 | 44 | # File extensions 45 | *.mbtiles 46 | *.air 47 | *.ipa 48 | *.apk 49 | *.dab 50 | *.sh 51 | *.iml 52 | *.ipr 53 | *.iws 54 | *.tsbuildinfo 55 | *~ 56 | *.tar.gz 57 | -------------------------------------------------------------------------------- /ui/component/rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest", 3 | "version": "1.0.0", 4 | "description": "REST API", 5 | "main": "dist/umd/index.bundle.js", 6 | "private": true, 7 | "module": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "exports": { 10 | ".": "./lib/index.js", 11 | "./*": "./lib/*.js" 12 | }, 13 | "files": [ 14 | "lib", 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "npx tsc -b", 19 | "test": "echo \"No tests\" && exit 0", 20 | "prepublishOnly": "npx webpack" 21 | }, 22 | "author": "OpenRemote", 23 | "license": "AGPL-3.0-or-later", 24 | "dependencies": { 25 | "@openremote/rest": "^1.3.4", 26 | "axios": "0.24.0", 27 | "model": "workspace:*", 28 | "qs": "^6.8.0" 29 | }, 30 | "devDependencies": { 31 | "@openremote/util": "^1.3.4", 32 | "@types/qs": "^6.9.7" 33 | }, 34 | "publishConfig": { 35 | "access": "restricted" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/TeltonikaParameterData.java: -------------------------------------------------------------------------------- 1 | package telematics.teltonika; 2 | 3 | import org.openremote.model.teltonika.TeltonikaParameter; 4 | 5 | import java.util.Map; 6 | 7 | public class TeltonikaParameterData { 8 | String key; 9 | TeltonikaParameter value; 10 | 11 | public TeltonikaParameterData(String key, TeltonikaParameter value) { 12 | this.key = key; 13 | this.value = value; 14 | } 15 | 16 | public String getParameterId() { 17 | return key; 18 | } 19 | 20 | public TeltonikaParameter getParameter() { 21 | return value; 22 | } 23 | 24 | //override equals to compare only keys 25 | @Override 26 | public boolean equals(Object obj) { 27 | if (obj == this) { 28 | return true; 29 | } 30 | if (!(obj instanceof TeltonikaParameterData)) { 31 | return false; 32 | } 33 | return this.key.equals(((TeltonikaParameterData) obj).key); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/src/test/resources/teltonika/teltonikaValidPayload1.json: -------------------------------------------------------------------------------- 1 | { 2 | "state":{ 3 | "reported":{ 4 | "ts":1698922776030, 5 | "pr":1, 6 | "latlng":"0.000000,0.000000", 7 | "alt":0, 8 | "ang":0, 9 | "sat":0, 10 | "sp":0, 11 | "evt":250, 12 | "239":0, 13 | "240":1, 14 | "80":1, 15 | "21":4, 16 | "200":0, 17 | "69":2, 18 | "237":2, 19 | "113":100, 20 | "263":2, 21 | "303":1, 22 | "250":1, 23 | "181":0, 24 | "182":0, 25 | "66":11922, 26 | "24":0, 27 | "206":30, 28 | "67":4110, 29 | "68":0, 30 | "13":2840, 31 | "17":45, 32 | "18":-11, 33 | "19":988, 34 | "15":1000, 35 | "241":20416, 36 | "199":0, 37 | "16":6870, 38 | "12":244, 39 | "449":0, 40 | "636":53977651, 41 | "11":893116211, 42 | "238":"0x0000000000000000", 43 | "14":1680671168, 44 | "387":"+000000.0000+0000000.0000+000.000/" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CustomValueTypes.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.custom; 2 | 3 | import org.openremote.model.teltonika.TeltonikaDataPayloadModel; 4 | import org.openremote.model.teltonika.TeltonikaParameter; 5 | import org.openremote.model.value.ValueDescriptor; 6 | 7 | import java.util.HashMap; 8 | 9 | public class CustomValueTypes { 10 | public static final ValueDescriptor ASSET_STATE_DURATION = new ValueDescriptor<>("AssetStateDuration", AssetStateDuration.class); 11 | public static final ValueDescriptor TELTONIKA_PARAMETER = new ValueDescriptor<>("TeltonikaParameter", TeltonikaParameter.class); 12 | public static class TeltonikaParameterMap extends HashMap {} 13 | 14 | public static final ValueDescriptor TELTONIKA_PARAMETER_MAP = new ValueDescriptor<>("TeltonikaParameterMap", TeltonikaParameterMap.class); 15 | 16 | public static final ValueDescriptor TELTONIKA_PAYLOAD = new ValueDescriptor<>("TeltonikaPayload", TeltonikaDataPayloadModel.class); 17 | } 18 | -------------------------------------------------------------------------------- /profile/dev-testing.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for doing keycloak custom theme development 4 | # 5 | # Please see profile/deploy.yml for configuration details for each service. 6 | # 7 | services: 8 | 9 | keycloak: 10 | extends: 11 | file: ../openremote/profile/deploy.yml 12 | service: keycloak 13 | volumes: 14 | # Map custom themes 15 | - ../deployment:/deployment 16 | # Access directly if needed on localhost 17 | ports: 18 | - "8081:8080" 19 | depends_on: 20 | postgresql: 21 | condition: service_healthy 22 | environment: 23 | KC_HOSTNAME_STRICT_HTTPS: 'false' 24 | KC_HOSTNAME_PORT: ${KC_HOSTNAME_PORT:-8080} 25 | # Prevent theme caching during dev 26 | KEYCLOAK_START_OPTS: --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false 27 | 28 | postgresql: 29 | extends: 30 | file: ../openremote/profile/deploy.yml 31 | service: postgresql 32 | volumes: 33 | - ../openremote/tmp:/storage 34 | # Access directly if needed on localhost 35 | ports: 36 | - "5432:5432" 37 | -------------------------------------------------------------------------------- /ui/app/custom/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | 3 | api.cache(false); 4 | return { 5 | presets: [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | modules: false, 10 | exclude: ['transform-async-to-generator'], 11 | targets: [ 12 | "last 2 versions", 13 | "IE 11" 14 | ] 15 | } 16 | ] 17 | ], 18 | plugins: [ 19 | [ 20 | "@babel/transform-runtime", 21 | { 22 | "corejs": false, 23 | "helpers": true, 24 | "regenerator": true, 25 | "useESModules": false 26 | } 27 | ], 28 | "@babel/syntax-dynamic-import", 29 | "@babel/syntax-object-rest-spread", 30 | [ 31 | 'module:fast-async', 32 | { 33 | spec: true 34 | } 35 | ] 36 | ] 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/ITeltonikaPayload.java: -------------------------------------------------------------------------------- 1 | package telematics.teltonika; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import org.openremote.container.timer.TimerService; 5 | import org.openremote.model.asset.Asset; 6 | import org.openremote.model.attribute.Attribute; 7 | import org.openremote.model.attribute.AttributeMap; 8 | import org.openremote.model.value.AttributeDescriptor; 9 | 10 | import java.util.Map; 11 | import java.util.logging.Logger; 12 | 13 | public interface ITeltonikaPayload { 14 | 15 | String getModelNumber(); 16 | /** 17 | * Returns list of attributes depending on the Teltonika JSON Payload. 18 | * Uses the logic and results from parsing the Teltonika Parameter IDs. 19 | * 20 | * @return Map of {@link Attribute}s to be assigned to the {@link Asset}. 21 | */ 22 | Map getAttributesFromPayload(TeltonikaConfiguration config, TimerService timerService) throws JsonProcessingException; 23 | 24 | AttributeMap getAttributes(Map payloadMap, TeltonikaConfiguration config, Logger logger, Map> descs); 25 | } 26 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/TeltonikaPayloadFactory.java: -------------------------------------------------------------------------------- 1 | package telematics.teltonika; 2 | 3 | 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import jakarta.validation.UnexpectedTypeException; 8 | import org.openremote.model.teltonika.TeltonikaDataPayloadModel; 9 | 10 | public class TeltonikaPayloadFactory { 11 | public static ITeltonikaPayload getPayload(String payload, String modelNumber) throws JsonProcessingException { 12 | ObjectMapper mapper = new ObjectMapper(); 13 | JsonNode rootNode = new ObjectMapper().readTree(payload); 14 | if (rootNode.has("state")) { 15 | // This looks like a DataPayload. 16 | TeltonikaDataPayloadModel model = mapper.readValue(payload, TeltonikaDataPayloadModel.class); 17 | return new TeltonikaDataPayload(model.state, modelNumber); 18 | } else if (rootNode.has("RSP")) { 19 | // This looks like an SMSPayload. 20 | return mapper.readValue(payload, TeltonikaResponsePayload.class); 21 | } else { 22 | throw new UnexpectedTypeException("Unknown type for data payload"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /agent/src/main/java/org/openremote/agent/teltonika/CustomAgentModelProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package org.openremote.agent.teltonika; 22 | 23 | import org.openremote.model.AssetModelProvider; 24 | 25 | public class CustomAgentModelProvider implements AssetModelProvider { 26 | 27 | @Override 28 | public boolean useAutoScan() { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/State.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import java.io.Serializable; 8 | import java.util.*; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonPropertyOrder({ 12 | "reported" 13 | }) 14 | public class State implements Serializable { 15 | 16 | // public ReportedState reportedState; 17 | @JsonProperty("reported") 18 | public Map reported; 19 | 20 | @Override 21 | public String toString() { 22 | StringBuilder sb = new StringBuilder(); 23 | sb.append(State.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('['); 24 | sb.append("reported"); 25 | sb.append('='); 26 | sb.append(((this.reported == null) ? "" : this.reported)); 27 | sb.append(','); 28 | if (sb.charAt((sb.length() - 1)) == ',') { 29 | sb.setCharAt((sb.length() - 1), ']'); 30 | } else { 31 | sb.append(']'); 32 | } 33 | return sb.toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CustomData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.model.custom; 21 | 22 | public class CustomData { 23 | 24 | protected String name; 25 | protected Integer age; 26 | 27 | protected CustomData() { 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public Integer getAge() { 35 | return age; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CustomEndpointResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.model.custom; 21 | 22 | import jakarta.ws.rs.POST; 23 | import jakarta.ws.rs.Path; 24 | 25 | /** 26 | * This is an example custom JAX-RS endpoint; this will be compiled and made available in the typescript model as well 27 | */ 28 | @Path("custom") 29 | public interface CustomEndpointResource { 30 | 31 | @POST 32 | void submitData(CustomData customData); 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci_cd.yml: -------------------------------------------------------------------------------- 1 | # This just references the CI/CD workflow in the main OpenRemote repo 2 | name: CI/CD 3 | 4 | on: 5 | # Push excluding tags and workflow changes 6 | push: 7 | branches: 8 | - 'main' 9 | tags-ignore: 10 | - '*.*' 11 | paths-ignore: 12 | - '.github/**' 13 | - '.ci_cd/**' 14 | - '**/*.md' 15 | 16 | # PR 17 | pull_request: 18 | 19 | # When a release is published 20 | release: 21 | types: [published] 22 | 23 | # Manual trigger 24 | workflow_dispatch: 25 | inputs: 26 | ENVIRONMENT: 27 | description: 'Environment to use (if any)' 28 | CLEAN_INSTALL: 29 | description: 'Delete data before starting' 30 | type: boolean 31 | COMMIT: 32 | description: 'Repo branch or commit SHA to checkout' 33 | OR_HOSTNAME: 34 | description: 'Host to deploy to (e.g. demo.openremote.app)' 35 | OR_ADMIN_PASSWORD: 36 | description: 'Admin password override' 37 | 38 | # Un-comment to monitor manager docker image tags for changes and trigger a redeploy when they change (see .ci_cd/README.md) 39 | # schedule: 40 | # - cron: '*/17 * * * *' 41 | 42 | jobs: 43 | call-main-repo: 44 | name: CI/CD 45 | uses: openremote/openremote/.github/workflows/ci_cd.yml@master 46 | with: 47 | INPUTS: ${{ toJSON(github.event.inputs) }} 48 | secrets: 49 | SECRETS: ${{ toJSON(secrets) }} 50 | -------------------------------------------------------------------------------- /ui/component/model/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath "cz.habarta.typescript-generator:typescript-generator-gradle-plugin:$typescriptGeneratorVersion" 8 | } 9 | } 10 | 11 | plugins { 12 | id 'groovy' 13 | id 'cz.habarta.typescript-generator' 14 | } 15 | 16 | dependencies { 17 | compileOnly "io.openremote:openremote-model-util:$openremoteVersion" 18 | implementation project(":agent") 19 | implementation project(":model") 20 | implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion" 21 | implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" 22 | implementation "com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonVersion" 23 | implementation "cz.habarta.typescript-generator:typescript-generator-core:$typescriptGeneratorVersion" 24 | } 25 | 26 | generateTypeScript createTSGeneratorConfigForModel("src/model.ts", findProject(":model")) 27 | 28 | build.dependsOn generateTypeScript, npmBuild 29 | npmBuild.dependsOn generateTypeScript 30 | 31 | clean { 32 | doLast { 33 | def dir = new File("${projectDir}/dist") 34 | dir.deleteDir() 35 | } 36 | } 37 | 38 | tasks.register('prepareUi') { 39 | dependsOn clean, npmPrepare 40 | } 41 | 42 | tasks.register('publishUi') { 43 | dependsOn clean, npmPublish 44 | } 45 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CustomAssetModelProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.model.custom; 21 | 22 | import org.openremote.model.AssetModelProvider; 23 | import org.openremote.model.ModelDescriptor; 24 | import org.openremote.model.ModelDescriptors; 25 | import org.openremote.model.asset.Asset; 26 | 27 | @ModelDescriptors({@ModelDescriptor(assetType = Asset.class, provider = CustomValueTypes.class)}) 28 | public class CustomAssetModelProvider implements AssetModelProvider { 29 | 30 | @Override 31 | public boolean useAutoScan() { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/app/custom/webpack.config.js: -------------------------------------------------------------------------------- 1 | const util = require("@openremote/util"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | const webpack = require("webpack"); 4 | 5 | module.exports = (env, argv) => { 6 | 7 | const customConfigDir = env.config; 8 | const managerUrl = env.managerUrl; 9 | const keycloakUrl = "http://localhost:8080/auth"; 10 | const IS_DEV_SERVER = process.argv.find(arg => arg.includes("serve")); 11 | const config = util.getAppConfig(argv.mode, IS_DEV_SERVER, __dirname, managerUrl, keycloakUrl); 12 | 13 | if (IS_DEV_SERVER && customConfigDir) { 14 | console.log("CUSTOM_CONFIG_DIR: " + customConfigDir); 15 | // Try and include the static files in the specified config dir if we're in dev server mode 16 | config.plugins.push(new CopyWebpackPlugin({ 17 | patterns: [ 18 | { 19 | from: customConfigDir 20 | }, 21 | ] 22 | })); 23 | } 24 | 25 | config.plugins.push(new CopyWebpackPlugin({ 26 | patterns: [ 27 | { from: 'locales', to: 'locales' } 28 | ] 29 | })); 30 | 31 | // Add a custom base URL to resolve the config dir to the path of the dev server not root 32 | config.plugins.push( 33 | new webpack.DefinePlugin({ 34 | CONFIG_URL_PREFIX: JSON.stringify(IS_DEV_SERVER && customConfigDir ? "/manager" : "") 35 | }) 36 | ); 37 | 38 | return config; 39 | }; 40 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/IMEIValidator.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | /** 4 | * Class that validates an IMEI value. 5 | * Employs checksum as outlined by the GSM Association. 6 | * Class retrieved from GeeksForGeeks. 7 | * 8 | * @see the GSMA IMEI Allocation Guidelines. 9 | * 10 | */ 11 | public class IMEIValidator { 12 | // Function for finding and returning 13 | // sum of digits of a number 14 | static int sumDig(int n) { 15 | int a = 0; 16 | while (n > 0) { 17 | a = a + n % 10; 18 | n = n / 10; 19 | } 20 | return a; 21 | } 22 | 23 | public static boolean isValidIMEI(long n) { 24 | // Converting the number into String 25 | // for finding length 26 | String s = Long.toString(n); 27 | int len = s.length(); 28 | 29 | if (len != 15) 30 | return false; 31 | 32 | int sum = 0; 33 | for (int i = len; i >= 1; i--) { 34 | int d = (int) (n % 10); 35 | 36 | // Doubling every alternate digit 37 | if (i % 2 == 0) 38 | d = 2 * d; 39 | 40 | // Finding sum of the digits 41 | sum += sumDig(d); 42 | n = n / 10; 43 | } 44 | 45 | return (sum % 10 == 0); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /setup/src/main/java/org/openremote/manager/setup/custom/CustomManagerSetup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.manager.setup.custom; 21 | 22 | import org.openremote.manager.setup.ManagerSetup; 23 | import org.openremote.model.Constants; 24 | import org.openremote.model.Container; 25 | import org.openremote.model.asset.impl.ThingAsset; 26 | 27 | public class CustomManagerSetup extends ManagerSetup { 28 | 29 | public CustomManagerSetup(Container container) { 30 | super(container); 31 | } 32 | 33 | @Override 34 | public void onStart() throws Exception { 35 | super.onStart(); 36 | 37 | // Create assets on clean startup here 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/PayloadJsonObject.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "state" 11 | }) 12 | public class PayloadJsonObject { 13 | 14 | @JsonProperty("state") 15 | public State state; 16 | @JsonIgnore 17 | private Map additionalProperties = new LinkedHashMap(); 18 | 19 | @JsonAnyGetter 20 | public Map getAdditionalProperties() { 21 | return this.additionalProperties; 22 | } 23 | 24 | @JsonAnySetter 25 | public void setAdditionalProperty(String name, Object value) { 26 | this.additionalProperties.put(name, value); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | StringBuilder sb = new StringBuilder(); 32 | sb.append(PayloadJsonObject.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('['); 33 | sb.append("state"); 34 | sb.append('='); 35 | sb.append(((this.state == null) ? "" : this.state)); 36 | sb.append(','); 37 | sb.append("additionalProperties"); 38 | sb.append('='); 39 | sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties)); 40 | sb.append(','); 41 | if (sb.charAt((sb.length() - 1)) == ',') { 42 | sb.setCharAt((sb.length() - 1), ']'); 43 | } else { 44 | sb.append(']'); 45 | } 46 | return sb.toString(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /profile/dev-ui.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for doing UI development work. 4 | # 5 | # Please see profile/deploy.yml for configuration details for each service. 6 | # 7 | volumes: 8 | manager-data: 9 | 10 | services: 11 | 12 | keycloak: 13 | extends: 14 | file: ../openremote/profile/deploy.yml 15 | service: keycloak 16 | volumes: 17 | # Map custom themes 18 | - ../deployment:/deployment 19 | # Access directly if needed on localhost 20 | ports: 21 | - "8081:8080" 22 | depends_on: 23 | postgresql: 24 | condition: service_healthy 25 | environment: 26 | KC_HOSTNAME: ${OR_HOSTNAME:-localhost} 27 | KC_HOSTNAME_STRICT_HTTPS: 'false' 28 | KC_HOSTNAME_PORT: ${KC_HOSTNAME_PORT:-8080} 29 | # Prevent theme caching during dev 30 | KEYCLOAK_START_OPTS: --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false 31 | 32 | postgresql: 33 | extends: 34 | file: ../openremote/profile/deploy.yml 35 | service: postgresql 36 | volumes: 37 | - manager-data:/storage 38 | # Access directly if needed on localhost 39 | ports: 40 | - "5432:5432" 41 | 42 | manager: 43 | extends: 44 | file: ../openremote/profile/deploy.yml 45 | service: manager 46 | depends_on: 47 | postgresql: 48 | condition: service_healthy 49 | volumes: 50 | - manager-data:/storage 51 | - ../deployment/build/image:/deployment 52 | environment: 53 | OR_SETUP_RUN_ON_RESTART: ${OR_SETUP_RUN_ON_RESTART:-true} 54 | OR_DEV_MODE: ${OR_DEV_MODE:-true} 55 | KC_HOSTNAME_PORT: ${KC_HOSTNAME_PORT:-8080} 56 | ports: 57 | - "8080:8080" 58 | -------------------------------------------------------------------------------- /deployment/manager/README.md: -------------------------------------------------------------------------------- 1 | ## Manager customisation 2 | As well as the below information please see the [Manager endpoints and file paths](https://github.com/openremote/openremote/wiki/Architecture:-Manager-endpoints-and-file-paths) wiki. 3 | 4 | ### Custom provisioning files (`provisioning/`) 5 | As an alternative to writing `java` setup code you can also provide `json` representations of Assets which will be automatically deserialized and added to the system when doing a clean install. 6 | 7 | ### Console App Configurations (`consoleappconfig/`) 8 | Console app configurations that can be loaded by Android and iOS consoles. 9 | 10 | ### Custom App Files (`app/`) 11 | This `app` directory is used as the `$CUSTOM_APP_DOCROOT` and can be used to store any custom static content; the Manager UI also checks the `/manager_config.json` path for a custom Manager UI configuration `json` file. 12 | 13 | ### FCM Configuration (`fcm.json`) 14 | This is where your Firebase cloud messaging config file should be placed to enable push notification for Android/iOS. 15 | 16 | ### Logging Configuration (`logging.properties`) 17 | Custom `JUL` logging configuration file; default log file can be found [here](https://github.com/openremote/openremote/blob/master/manager/src/main/resources/logging.properties). 18 | 19 | ### Keycloak Credentials (`keycloak.json`) 20 | This is where custom keycloak credentials are stored/can be supplied; by default the manager will auto generate these during a clean install. 21 | 22 | ### Custom Java Code (`extensions/`) 23 | Any custom java code should be compiled and made available in this directory; if it is compiled as part of the custom project then only the compiled code should be copied to the `deployment/build/image/manager/extensions` dir and not to this source directory. 24 | -------------------------------------------------------------------------------- /setup/src/main/java/org/openremote/manager/setup/custom/CustomKeycloakSetup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.manager.setup.custom; 21 | 22 | import org.openremote.manager.setup.AbstractKeycloakSetup; 23 | import org.openremote.model.Constants; 24 | import org.openremote.model.Container; 25 | import org.openremote.model.security.User; 26 | 27 | public class CustomKeycloakSetup extends AbstractKeycloakSetup { 28 | 29 | protected User serviceUser; 30 | 31 | public CustomKeycloakSetup(Container container, boolean isProduction) { 32 | super(container); 33 | } 34 | 35 | @Override 36 | public void onStart() throws Exception { 37 | 38 | serviceUser = new User() 39 | .setServiceAccount(true) 40 | .setEnabled(true) 41 | .setUsername("serviceUser"); 42 | 43 | serviceUser = keycloakProvider.createUpdateUser(Constants.MASTER_REALM, serviceUser, null, true); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /setup/src/main/java/org/openremote/manager/setup/custom/CustomSetupTasks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.manager.setup.custom; 21 | 22 | import org.openremote.model.Container; 23 | import org.openremote.model.setup.Setup; 24 | import org.openremote.model.setup.SetupTasks; 25 | 26 | import java.util.Arrays; 27 | import java.util.List; 28 | 29 | public class CustomSetupTasks implements SetupTasks { 30 | 31 | public static final String PRODUCTION = "production"; 32 | 33 | @Override 34 | public List createTasks(Container container, String setupType, boolean keycloakEnabled) { 35 | 36 | boolean isProduction = PRODUCTION.equalsIgnoreCase(setupType); 37 | 38 | // Add custom Setup task implementations here with tasks optionally dependent on setupType 39 | return Arrays.asList( 40 | new CustomKeycloakSetup(container, isProduction), 41 | new CustomManagerSetup(container) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /deployment/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java-library" 2 | 3 | configurations { 4 | managerRuntime 5 | } 6 | 7 | dependencies { 8 | api project(":setup") 9 | managerRuntime "io.openremote:openremote-manager:$openremoteVersion" 10 | } 11 | 12 | tasks.register('license') { 13 | doLast { 14 | def licenseFiles = new ArrayList<>() 15 | licenseFiles.add("${rootDir}/LICENSE.txt") 16 | 17 | def toConcatenate = files(licenseFiles.toArray()) 18 | def outputFileName = "${buildDir}/image/manager/app/LICENSE.txt" 19 | def output = new File(outputFileName) 20 | if (output.exists()) { 21 | output.delete() 22 | } 23 | output.getParentFile().mkdirs() 24 | output.createNewFile() 25 | output.write('') // truncate output if needed 26 | toConcatenate.each { f -> output << f.text } 27 | } 28 | } 29 | 30 | tasks.register('installDist', Copy) { 31 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 32 | dependsOn(parent.getTasksByName('installDist', true).findAll { 33 | // Don't create circular dependency or depend on built in openremote submodule apps 34 | it.project != project && !it.project.path.startsWith(":openremote:ui:app") 35 | }) 36 | 37 | into("$buildDir") 38 | 39 | from "Dockerfile" 40 | 41 | into("image") { 42 | from projectDir 43 | exclude "build.gradle", "Dockerfile", "build", "**/*.mbtiles", "src", "**/*.md", ".gitignore", "**/*.encrypted" 44 | } 45 | 46 | into("image/manager/extensions") { 47 | from configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.collect {it.file } 48 | // Don't include deps baked into the manager docker image 49 | exclude configurations.managerRuntime.resolvedConfiguration.resolvedArtifacts.collect {it.file.name } 50 | // Don't include any ui packaged JARs (used for dev purposes only) 51 | exclude { (it.file.path.contains(".gradle") || it.file.path.contains(".m2")) && it.file.path.contains("io.openremote.ui") } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/app/custom/src/pages/page-custom.ts: -------------------------------------------------------------------------------- 1 | import {css, html, TemplateResult} from "lit"; 2 | import {customElement} from "lit/decorators.js"; 3 | import {AppStateKeyed, Page, PageProvider} from "@openremote/or-app"; 4 | import {EnhancedStore} from "@reduxjs/toolkit"; 5 | import {CustomData} from "model"; 6 | import rest from "rest"; 7 | 8 | export function pageCustomProvider(store: EnhancedStore): PageProvider { 9 | return { 10 | name: "customPage", 11 | routes: [ 12 | "custom1" 13 | ], 14 | pageCreator: () => { 15 | const page = new PageCustom(store); 16 | return page; 17 | } 18 | }; 19 | } 20 | 21 | 22 | @customElement("page-custom") 23 | export class PageCustom extends Page { 24 | 25 | static get styles() { 26 | // language=CSS 27 | return css` 28 | :host { 29 | display: flex; 30 | align-items: start; 31 | flex-direction: column; 32 | width: 100%; 33 | --or-icon-fill: var(--or-app-color4); 34 | } 35 | `; 36 | } 37 | 38 | get name(): string { 39 | return "custom"; 40 | } 41 | 42 | constructor(store: EnhancedStore) { 43 | super(store); 44 | } 45 | 46 | protected render(): TemplateResult | void { 47 | return html` 48 |

This is an example custom page with a custom app translation for 'customString'

49 | ` 50 | } 51 | 52 | protected firstUpdated(changedProperties: Map) { 53 | let submitter = async() => { 54 | // Crude custom REST API call example 55 | return await rest.api.CustomEndpointResource.submitData({ 56 | name: "Commander Data", 57 | age: 1234 58 | }); 59 | } 60 | 61 | submitter() 62 | .then(() => console.log("Successfully posted to custom endpoint")) 63 | .catch((reason) => console.log("Failed to post to custom endpoint")); 64 | } 65 | 66 | stateChanged(state: AppStateKeyed): void { 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/build.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | 4 | plugins { 5 | id 'com.adarshr.test-logger' version '3.1.0' 6 | } 7 | 8 | apply plugin: "java-library" 9 | apply plugin: "groovy" 10 | 11 | dependencies { 12 | api project(":manager") 13 | api project(":model") 14 | api project(":setup") 15 | testImplementation "io.openremote:openremote-test:$openremoteVersion" 16 | } 17 | 18 | tasks.withType(Test) { 19 | environment("LOGGING_CONFIG_FILE", "test/src/logging-test.properties") 20 | } 21 | 22 | test { 23 | workingDir = findProject(":openremote") != null ? project(":openremote").projectDir : rootProject.projectDir 24 | useJUnitPlatform() 25 | testLogging { 26 | // set options for log level LIFECYCLE 27 | events TestLogEvent.FAILED, 28 | TestLogEvent.PASSED, 29 | TestLogEvent.SKIPPED 30 | exceptionFormat TestExceptionFormat.FULL 31 | showExceptions true 32 | showCauses true 33 | showStackTraces true 34 | 35 | // set options for log level DEBUG and INFO 36 | debug { 37 | events TestLogEvent.STARTED, 38 | TestLogEvent.FAILED, 39 | TestLogEvent.PASSED, 40 | TestLogEvent.SKIPPED, 41 | TestLogEvent.STANDARD_ERROR 42 | exceptionFormat TestExceptionFormat.FULL 43 | } 44 | info.events = debug.events 45 | info.exceptionFormat = debug.exceptionFormat 46 | 47 | afterTest { desc, result -> 48 | logger.quiet "${desc.className} > ${desc.name} took: ${(result.endTime - result.startTime)}ms" 49 | } 50 | 51 | afterSuite { desc, result -> 52 | if (!desc.parent) { // will match the outermost suite 53 | def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" 54 | def startItem = '| ', endItem = ' |' 55 | def repeatLength = startItem.length() + output.length() + endItem.length() 56 | println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /profile/dev-proxy.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for running the reverse proxy on https://localhost/ with the manager backend mapped to the host machine on 4 | # localhost:8080 and the keycloak backend mapped to the host machine on localhost:8081 5 | # 6 | # Your changes will be visible live on browser reload or after restart: 7 | # 8 | # - Run the manager in an IDE with the following required environment variables: 9 | # OR_WEBSERVER_LISTEN_HOST=0.0.0.0 10 | # 11 | # Please see deploy.yml for configuration details for each service. 12 | # 13 | volumes: 14 | postgresql-data: 15 | 16 | services: 17 | 18 | proxy: 19 | extends: 20 | file: deploy.yml 21 | service: proxy 22 | environment: 23 | MANAGER_HOST: 'host.docker.internal' 24 | # Uncomment to use sish in development 25 | #SISH_HOST: sish 26 | #SISH_PORT: 8090 27 | 28 | keycloak: 29 | extends: 30 | file: deploy.yml 31 | service: keycloak 32 | #volumes: 33 | # Map custom themes 34 | # - ../deployment:/deployment 35 | # Access directly if needed on localhost 36 | ports: 37 | - "8081:8080" 38 | # Following options are useful for tunnelling functionality do not use on public instance 39 | environment: 40 | KEYCLOAK_ISSUER_BASE_URI: https://localhost/auth 41 | KC_HOSTNAME_STRICT: false 42 | KC_HOSTNAME: 43 | KC_LOG_CONSOLE_FORMAT: '%-5p [%c] (%t) %s%e%n' 44 | depends_on: 45 | postgresql: 46 | condition: service_healthy 47 | 48 | postgresql: 49 | extends: 50 | file: deploy.yml 51 | service: postgresql 52 | volumes: 53 | - ../tmp:/storage 54 | # Access directly if needed on localhost 55 | ports: 56 | - "5432:5432" 57 | 58 | # Uncomment to use sish in development tunnels won't be usable without wildcard dns but the start/stop tunnel logic can be tested 59 | # sish: 60 | # image: antoniomika/sish:latest 61 | # depends_on: 62 | # - proxy 63 | # healthcheck: 64 | # test: ["CMD", "/app/app", "-v"] 65 | # ports: 66 | # - "2222:2222" 67 | # - "9000-9002:9000-9002" 68 | # command: | 69 | # --ssh-address=:2222 70 | # --http-address=:8090 71 | # --https=false 72 | # --verify-ssl=false 73 | # --idle-connection=false 74 | # --port-bind-range="9000-9002" 75 | # --bind-random-ports=false 76 | # --bind-random-subdomains=false 77 | # --force-requested-subdomains=true 78 | # restart: always 79 | -------------------------------------------------------------------------------- /ui/app/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openremote/custom", 3 | "version": "1.0.0", 4 | "description": "OpenRemote Custom App", 5 | "author": "OpenRemote", 6 | "license": "AGPL-3.0-or-later", 7 | "private": true, 8 | "exports": { 9 | "./*": "./lib/*.js" 10 | }, 11 | "scripts": { 12 | "clean": "npx tsc -b --clean && npx shx rm -rf dist", 13 | "modelBuild": "npx orutil build", 14 | "modelWatch": "npx orutil watch", 15 | "build": "npx cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack --mode production", 16 | "serve": "npx tsc -b --clean && npx shx rm -rf dist && npx orutil build && npx cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack serve --mode development", 17 | "serveNoModelBuild": "npx tsc -b --clean && npx cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack serve --mode development", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "dependencies": { 21 | "@openremote/manager": "^1.3.4", 22 | "model": "workspace:*", 23 | "rest": "workspace:*" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.16.0", 27 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 28 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3", 29 | "@babel/plugin-transform-regenerator": "^7.16.0", 30 | "@babel/plugin-transform-runtime": "^7.16.4", 31 | "@babel/preset-env": "^7.16.4", 32 | "@babel/runtime": "^7.16.3", 33 | "@openremote/util": "^1.3.4", 34 | "@typescript-eslint/eslint-plugin": "^5.9.0", 35 | "@typescript-eslint/parser": "^5.9.0", 36 | "babel-loader": "^8.2.3", 37 | "copy-webpack-plugin": "^10.0.0", 38 | "cross-env": "^7.0.3", 39 | "css-loader": "^6.5.1", 40 | "eslint": "^8.6.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-config-standard": "^16.0.3", 43 | "eslint-plugin-import": "^2.25.4", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^6.0.0", 46 | "fast-async": "^6.3.8", 47 | "file-loader": "^6.2.0", 48 | "fork-ts-checker-notifier-webpack-plugin": "^4.0.0", 49 | "fork-ts-checker-webpack-plugin": "^6.5.0", 50 | "html-webpack-plugin": "^5.5.0", 51 | "prettier": "^2.5.1", 52 | "querystring-es3": "^0.2.1", 53 | "shx": "^0.3.3", 54 | "source-map-loader": "^3.0.0", 55 | "style-loader": "^3.3.1", 56 | "ts-loader": "^9.2.6", 57 | "typescript": ">=4.5.2", 58 | "webpack": "^5.76.0", 59 | "webpack-cli": "^4.9.1", 60 | "webpack-dev-server": "^4.5.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Fleet Management 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Docker Buildx 14 | uses: docker/setup-buildx-action@v1 15 | 16 | - name: Login to Docker Hub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKER_USERNAME }} 20 | password: ${{ secrets.DOCKER_PASSWORD }} 21 | 22 | - name: Get the git deployment version 23 | id: git_version 24 | run: echo "GIT_DEPLOYMENT_VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 25 | 26 | - name: Set up JDK 11 27 | uses: actions/setup-java@v2 28 | with: 29 | java-version: '17' 30 | distribution: 'adopt' 31 | 32 | - name: Grant execute permission for gradlew 33 | run: chmod +x ./gradlew 34 | 35 | - name: Clean and Install Distribution 36 | run: ./gradlew clean installDist 37 | 38 | - name: Build and push Fleet Deployment Image 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: ./deployment/build/ 42 | file: ./deployment/build/Dockerfile 43 | platforms: linux/amd64,linux/arm64 44 | push: true 45 | tags: | 46 | pankalog/test-deployment:${{ env.GIT_DEPLOYMENT_VERSION }} 47 | pankalog/test-deployment:latest 48 | 49 | - name: Build and push Fleet Management Image 50 | uses: docker/build-push-action@v2 51 | with: 52 | context: ./openremote/manager/build/install/manager/ 53 | file: ./openremote/manager/build/install/manager/Dockerfile 54 | platforms: linux/amd64,linux/arm64 55 | push: true 56 | tags: | 57 | ${{ secrets.DOCKER_USERNAME }}/fleet-test:${{ env.GIT_DEPLOYMENT_VERSION }} 58 | ${{ secrets.DOCKER_USERNAME }}/fleet-test:latest 59 | 60 | - name: Create Release 61 | id: create_release 62 | uses: actions/create-release@v1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | with: 66 | tag_name: ${{ env.GIT_DEPLOYMENT_VERSION }} 67 | release_name: Release ${{ env.GIT_DEPLOYMENT_VERSION }} 68 | body: | 69 | ## Changes 70 | ${{ env.COMMIT_LOG }} 71 | 72 | ## Docker Images 73 | - [Fleet Deployment Image](https://hub.docker.com/r/{{ secrets.DOCKER_USERNAME }}/test-deployment/tags?page=1&name=${{ env.GIT_DEPLOYMENT_VERSION }}) 74 | - [Fleet Management Image](https://hub.docker.com/r/{{ secrets.DOCKER_USERNAME }}/fleet-test/tags?page=1&name=${{ env.GIT_DEPLOYMENT_VERSION }}) -------------------------------------------------------------------------------- /ui/component/rest/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "groovy" 2 | apply plugin: "cz.habarta.typescript-generator" 3 | 4 | buildscript { 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath "cz.habarta.typescript-generator:typescript-generator-gradle-plugin:$typescriptGeneratorVersion" 10 | } 11 | } 12 | 13 | dependencies { 14 | compileOnly "io.openremote:openremote-model-util:$openremoteVersion" 15 | implementation project(":agent") 16 | implementation project(":model") 17 | implementation "cz.habarta.typescript-generator:typescript-generator-core:$typescriptGeneratorVersion" 18 | implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion" 19 | implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" 20 | implementation "com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonVersion" 21 | } 22 | 23 | generateTypeScript createTSGeneratorConfigForClient("src/restclient.ts", new File("${project(":ui:component:model").projectDir}/src/typescript-generator-info.json"), project(":model")) 24 | generateTypeScript.dependsOn resolveTask(":ui:component:model:generateTypeScript") 25 | generateTypeScript.onlyIf { countResourceClasses() > 0 } 26 | npmBuild.onlyIf { countResourceClasses() > 0 } 27 | tasks.register('dummyRestClient') { 28 | onlyIf { countResourceClasses() == 0 } 29 | doLast { 30 | file("${projectDir}/src/restclient.ts").text = 'export class ApiClient {}' 31 | } 32 | } 33 | generateTypeScript.finalizedBy dummyRestClient 34 | 35 | def countResourceClasses() { 36 | // Get the main source set's output classes directory 37 | def classesDir = project(":model").sourceSets.main.output.classesDirs 38 | def count = 0 39 | 40 | // Iterate through all class files 41 | classesDir.files.each { dir -> 42 | dir.eachFileRecurse { file -> 43 | if (file.name.endsWith('.class')) { 44 | // Convert file path to class name and check if it ends with "Resource" 45 | def className = file.absolutePath 46 | .substring(classesDir.asPath.length() + 1) 47 | .replace(File.separator, '.') 48 | .replace('.class', '') 49 | 50 | if (className.endsWith('Resource')) { 51 | count++ 52 | } 53 | 54 | } 55 | } 56 | } 57 | 58 | println "Total classes ending with 'Resource': $count" 59 | return count 60 | } 61 | 62 | clean { 63 | doLast { 64 | def dir = new File("${projectDir}/dist") 65 | dir.deleteDir() 66 | } 67 | } 68 | 69 | build.dependsOn generateTypeScript, npmBuild 70 | npmBuild.dependsOn generateTypeScript 71 | 72 | tasks.register('prepareUi') { 73 | dependsOn clean, build, npmPrepare 74 | } 75 | 76 | tasks.register('publishUi') { 77 | dependsOn clean, build, npmPublish 78 | } 79 | -------------------------------------------------------------------------------- /model-util/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id "maven-publish" 4 | id "signing" 5 | id 'cz.habarta.typescript-generator' 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation project(":model") 14 | implementation project(":agent") 15 | implementation "cz.habarta.typescript-generator:typescript-generator-core:$typescriptGeneratorVersion" 16 | } 17 | 18 | base { 19 | archivesName = "openremote-${project.name}" 20 | } 21 | 22 | jar { 23 | from sourceSets.main.allJava 24 | } 25 | 26 | javadoc { 27 | failOnError = false 28 | } 29 | 30 | java { 31 | withJavadocJar() 32 | withSourcesJar() 33 | } 34 | 35 | publishing { 36 | publications { 37 | maven(MavenPublication) { 38 | group = "io.openremote" 39 | artifactId = "openremote-${project.name}" 40 | from components.java 41 | pom { 42 | name = 'OpenRemote Model Util' 43 | description = 'Provides utilities for building the typescript model used within OpenRemote and custom projects' 44 | url = 'https://github.com/openremote/openremote' 45 | licenses { 46 | license { 47 | name = 'GNU Affero General Public License v3.0' 48 | url = 'https://www.gnu.org/licenses/agpl-3.0.en.html' 49 | } 50 | } 51 | developers { 52 | developer { 53 | id = 'developers' 54 | name = 'Developers' 55 | email = 'developers@openremote.io' 56 | organization = 'OpenRemote' 57 | organizationUrl = 'https://openremote.io' 58 | } 59 | } 60 | scm { 61 | connection = 'scm:git:git://github.com/openremote/openremote.git' 62 | developerConnection = 'scm:git:ssh://github.com:openremote/openremote.git' 63 | url = 'https://github.com/openremote/openremote/tree/master' 64 | } 65 | } 66 | } 67 | } 68 | 69 | repositories { 70 | maven { 71 | if (!version.endsWith('-LOCAL')) { 72 | credentials { 73 | username = findProperty("publishUsername") 74 | password = findProperty("publishPassword") 75 | } 76 | } 77 | url = version.endsWith('-LOCAL') ? layout.buildDirectory.dir('repo') : version.endsWith('-SNAPSHOT') ? findProperty("snapshotsRepoUrl") : findProperty("releasesRepoUrl") 78 | } 79 | } 80 | } 81 | 82 | signing { 83 | def signingKey = findProperty("signingKey") 84 | def signingPassword = findProperty("signingPassword") 85 | if (signingKey && signingPassword) { 86 | useInMemoryPgpKeys(signingKey, signingPassword) 87 | sign publishing.publications.maven 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ui/app/custom/src/index.ts: -------------------------------------------------------------------------------- 1 | // Declare require method which we'll use for importing webpack resources (using ES6 imports will confuse typescript parser) 2 | import {combineReducers, configureStore} from "@reduxjs/toolkit"; 3 | import "@openremote/or-app"; 4 | import {AnyAction, appReducer, AppStateKeyed, HeaderConfig, HeaderItem, OrApp, PageProvider, RealmAppConfig} from "@openremote/or-app"; 5 | import {headerItemAccount, headerItemLanguage, headerItemLogout, headerItemMap, headerItemAssets} from "@openremote/manager/headers"; 6 | import {pageAssetsReducer, pageAssetsProvider} from "@openremote/manager/pages/page-assets"; 7 | import {pageMapReducer, pageMapProvider} from "@openremote/manager/pages/page-map"; 8 | import "./pages/page-custom"; 9 | import {pageCustomProvider} from "./pages/page-custom"; 10 | 11 | const rootReducer = combineReducers({ 12 | app: appReducer, 13 | map: pageMapReducer, 14 | assets: pageAssetsReducer 15 | }); 16 | 17 | type RootState = ReturnType; 18 | 19 | export const store = configureStore({ 20 | reducer: rootReducer 21 | }); 22 | 23 | const orApp = new OrApp(store); 24 | 25 | export const DefaultPagesConfig: PageProvider[] = [ 26 | pageMapProvider(store), // Standard manager map page 27 | pageAssetsProvider(store), // Standard manager asset page 28 | pageCustomProvider(store) // Custom page 29 | ]; 30 | 31 | // A new header for our custom page 32 | export function headerItemCustom(orApp: OrApp): HeaderItem { 33 | return { 34 | icon: "rhombus-split", 35 | href: "custom1", 36 | text: "app:customPage", 37 | }; 38 | } 39 | 40 | export const DefaultHeaderMainMenu: {[name: string]: HeaderItem} = { 41 | map: headerItemMap(orApp), 42 | assets: headerItemAssets(orApp), 43 | custom: headerItemCustom(orApp) 44 | }; 45 | 46 | export const DefaultHeaderSecondaryMenu: {[name: string]: HeaderItem} = { 47 | language: headerItemLanguage(orApp), 48 | account: headerItemAccount(orApp), 49 | logout: headerItemLogout(orApp) 50 | }; 51 | 52 | export const DefaultHeaderConfig: HeaderConfig = { 53 | mainMenu: Object.values(DefaultHeaderMainMenu), 54 | secondaryMenu: Object.values(DefaultHeaderSecondaryMenu) 55 | }; 56 | 57 | export const DefaultRealmConfig: RealmAppConfig = { 58 | appTitle: "Custom App", 59 | header: DefaultHeaderConfig, 60 | styles: ":host > * {--or-app-color2: #F0F0F0; --or-app-color3: #22211f; --or-app-color4: #e3000a; --or-app-color5: #CCCCCC;}", 61 | }; 62 | 63 | // Configure manager connection and i18next settings 64 | orApp.managerConfig = { 65 | realm: "custom", 66 | loadTranslations: ["app", "or"] 67 | }; 68 | 69 | // Configure app pages and per realm styling/settings 70 | orApp.appConfig = { 71 | pages: [...DefaultPagesConfig], 72 | languages: { 73 | en: "english", 74 | nl: "dutch" 75 | }, 76 | superUserHeader: DefaultHeaderConfig, 77 | realms: { 78 | default: {...DefaultRealmConfig, header: DefaultHeaderConfig} 79 | } 80 | }; 81 | 82 | document.body.appendChild(orApp); 83 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/VehicleAsset.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.custom; 2 | 3 | import jakarta.persistence.Entity; 4 | import org.openremote.model.Constants; 5 | import org.openremote.model.asset.Asset; 6 | import org.openremote.model.asset.AssetDescriptor; 7 | import org.openremote.model.attribute.MetaMap; 8 | import org.openremote.model.geo.GeoJSONPoint; 9 | import org.openremote.model.teltonika.TeltonikaModelConfigurationAsset; 10 | import org.openremote.model.value.AttributeDescriptor; 11 | import org.openremote.model.value.ValueType; 12 | import org.openremote.model.value.impl.ColourRGB; 13 | 14 | import java.util.Date; 15 | import java.util.Optional; 16 | /** 17 | * {@code VehicleAsset} is a custom asset type specifically intended for the fleet management use case of OpenRemote. 18 | * It is used as the base class of any subsequent vehicle asset types that should work using this integration. 19 | * The VehicleAsset class contains all required attributes and methods to be used by the Teltonika Telematics integration. 20 | * 21 | * In case the user wants to add more attributes to the vehicle asset, they can do so by extending the VehicleAsset class. 22 | * To view such an example, see the CarAsset class. 23 | */ 24 | @Entity 25 | public class VehicleAsset extends Asset { 26 | 27 | public static final AttributeDescriptor LOCATION = new AttributeDescriptor<>("location", ValueType.GEO_JSON_POINT) 28 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Location")); 29 | 30 | public static final AttributeDescriptor IMEI = new AttributeDescriptor<>("IMEI", ValueType.TEXT); 31 | public static final AttributeDescriptor LAST_CONTACT = new AttributeDescriptor<>("lastContact", ValueType.DATE_AND_TIME) 32 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Last message time")); 33 | public static final AttributeDescriptor MODEL_NUMBER = new AttributeDescriptor<>("modelNumber", ValueType.TEXT); 34 | 35 | public static final AttributeDescriptor DIRECTION = new AttributeDescriptor<>("direction", ValueType.DIRECTION) 36 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Direction")) 37 | .withUnits(Constants.UNITS_DEGREE); 38 | 39 | // Figure out a way to use the colour parameter for the color of the car on the map 40 | 41 | 42 | 43 | public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("car", null, VehicleAsset.class); 44 | 45 | protected VehicleAsset(){ 46 | } 47 | public VehicleAsset(String name){super(name);} 48 | 49 | public Optional getIMEI() { 50 | return getAttributes().getValue(IMEI); 51 | } 52 | public Optional getLastContact() { 53 | return getAttributes().getValue(LAST_CONTACT); 54 | } 55 | 56 | public Optional getModelNumber(){return getAttributes().getValue(MODEL_NUMBER);} 57 | 58 | public VehicleAsset setModelNumber(String value){ 59 | getAttributes().getOrCreate(MODEL_NUMBER).setValue(value); 60 | return this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/TeltonikaResponsePayload.java: -------------------------------------------------------------------------------- 1 | 2 | package telematics.teltonika; 3 | 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import org.openremote.container.timer.TimerService; 8 | import org.openremote.model.attribute.Attribute; 9 | import org.openremote.model.attribute.AttributeMap; 10 | import org.openremote.model.teltonika.TeltonikaParameter; 11 | import org.openremote.model.value.AttributeDescriptor; 12 | 13 | import java.util.Map; 14 | import java.util.logging.Logger; 15 | /** 16 | * This class is used to represent the payload from a Teltonika device when responding to an SMS message. 17 | * It is used to parse the payload and extract the response from the device. 18 | * It arrives in the format of {@code {"RSP":"OK"}}. 19 | *

20 | * It implements the {@code ITeltonikaPayload} interface, which is used to extract the payload's 21 | * attributes and create an attribute map. 22 | */ 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonPropertyOrder({ 25 | "RSP" 26 | }) 27 | public class TeltonikaResponsePayload implements ITeltonikaPayload { 28 | 29 | @JsonProperty("RSP") 30 | public String rsp; 31 | @Override 32 | public String toString() { 33 | StringBuilder sb = new StringBuilder(); 34 | sb.append(TeltonikaResponsePayload.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('['); 35 | sb.append("rsp"); 36 | sb.append('='); 37 | sb.append(((this.rsp == null)?"":this.rsp)); 38 | sb.append(','); 39 | if (sb.charAt((sb.length()- 1)) == ',') { 40 | sb.setCharAt((sb.length()- 1), ']'); 41 | } else { 42 | sb.append(']'); 43 | } 44 | return sb.toString(); 45 | } 46 | 47 | private String modelNumber; 48 | 49 | @Override 50 | public String getModelNumber() { 51 | return modelNumber; 52 | } 53 | public void setModelNumber(String modelNumber) { 54 | this.modelNumber = modelNumber; 55 | } 56 | 57 | @Override 58 | public Map getAttributesFromPayload(TeltonikaConfiguration config, TimerService timerService) { 59 | TeltonikaParameterData parameter = new TeltonikaParameterData( 60 | "RSP", 61 | //Create fake teltonikaparameter data 62 | new TeltonikaParameter(-1, "RSP", "-", "ASCII", "-", "-", "-", "-", "Response to an SMS message", "0", "0") 63 | ); 64 | return Map.of(parameter, rsp); 65 | } 66 | 67 | public AttributeMap getAttributes(Map payloadMap, TeltonikaConfiguration config, Logger logger, Map> descs) { 68 | AttributeMap attributeMap = new AttributeMap(); 69 | 70 | Attribute attribute = config.getResponseAttribute(); 71 | attribute.setValue((String) payloadMap.get(new TeltonikaParameterData("RSP", null))); 72 | attributeMap.put(attribute); 73 | return attributeMap; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/AssetStateDuration.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.custom; 2 | 3 | import org.openremote.model.asset.Asset; 4 | import org.openremote.model.value.MetaItemType; 5 | 6 | import java.io.Serializable; 7 | import java.sql.Timestamp; 8 | 9 | /** 10 | *

11 | * A {@link org.openremote.model.value.ValueType} that can store 2 and only 2 {@link java.time.LocalDateTime} values, 12 | * that indicate the start-time and the end-time of a time-constrained {@link Asset} state. 13 | * Its use is meant to assist in retrieving historical {@link org.openremote.model.datapoint.Datapoint}s between 14 | * two different {@link java.time.Instant}s. 15 | * 16 | *

17 | * By utilizing a {@link AssetStateDuration} {@link org.openremote.model.attribute.Attribute} 18 | * in conjunction with {@link MetaItemType#STORE_DATA_POINTS}, users can easily request the needed 19 | * {@link AssetStateDuration}s, for any given time duration, e.g. 30 days, with 20 | * which they can then request the specific periods for which an {@link Asset} was in a certain, user-defined, state. 21 | * 22 | *

23 | * As an example, assume a Bike-share service, and each {@link Asset} is a single bike. 24 | * At the end of a fiscal quarter, we would like to analyze the usage of each bicycle, and the time and duration 25 | * at which it moved, to then gauge the profitability of the bike. 26 | * 27 | * Instead of manually retrieving every single datapoint from the asset and then analyzing it to retrieve the value 28 | * changes, we can have an {@link AssetStateDuration} {@link org.openremote.model.attribute.Attribute}, which at 29 | * the end of any given session, or trip, stores the start-time and the end-time of the bike. 30 | * 31 | * As the Attribute has {@link MetaItemType#STORE_DATA_POINTS}, we can retrieve the {@link AssetStateDuration}s 32 | * for the quarter that passed, and for each of the {@link AssetStateDuration}s, request the data-points between 33 | * the start and end Timestamps. 34 | * 35 | * In this way, we have access to every single Duration that the Asset was at any given state. 36 | * 37 | * 38 | * We can then apply any sort of filtering on the set of {@link AssetStateDuration}s to retrieve our needed data. 39 | * 40 | *

41 | */ 42 | public class AssetStateDuration implements Serializable { 43 | private Timestamp startTime; 44 | private Timestamp endTime; 45 | 46 | public AssetStateDuration(Timestamp startTime, Timestamp endTime) { 47 | if (startTime == null || endTime == null) { 48 | throw new IllegalArgumentException("Start time and end time cannot be null"); 49 | } 50 | if (startTime.after(endTime)) { 51 | throw new IllegalArgumentException("Start time cannot be after end time"); 52 | } 53 | this.startTime = startTime; 54 | this.endTime = endTime; 55 | } 56 | 57 | public Timestamp getStartTime() { 58 | return startTime; 59 | } 60 | 61 | public Timestamp getEndTime() { 62 | return endTime; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return "AssetStateDuration{" + 68 | "startTime=" + startTime + 69 | ", endTime=" + endTime + 70 | '}'; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/TeltonikaConfigurationAsset.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | import jakarta.persistence.Entity; 4 | import org.openremote.model.asset.Asset; 5 | import org.openremote.model.asset.AssetDescriptor; 6 | import org.openremote.model.attribute.MetaItem; 7 | import org.openremote.model.attribute.MetaMap; 8 | import org.openremote.model.geo.GeoJSONPoint; 9 | import org.openremote.model.value.AttributeDescriptor; 10 | import org.openremote.model.value.MetaItemType; 11 | import org.openremote.model.value.ValueType; 12 | 13 | import java.util.Map; 14 | 15 | @Entity 16 | public class TeltonikaConfigurationAsset extends Asset { 17 | public static final AttributeDescriptor WHITELIST = new AttributeDescriptor<>("deviceIMEIWhitelist", ValueType.TEXT.asArray()).withOptional(true); 18 | public static final AttributeDescriptor ENABLED = new AttributeDescriptor<>("Enabled", ValueType.BOOLEAN); 19 | public static final AttributeDescriptor CHECK_FOR_IMEI = new AttributeDescriptor<>("CheckForValidIMEI", ValueType.BOOLEAN); 20 | public static final AttributeDescriptor STORE_PAYLOADS = new AttributeDescriptor<>("StorePayloads", ValueType.BOOLEAN); 21 | public static final AttributeDescriptor DEFAULT_MODEL_NUMBER = new AttributeDescriptor<>("defaultModelNumber", ValueType.TEXT); 22 | public static final AttributeDescriptor COMMAND = new AttributeDescriptor<>("command", ValueType.TEXT); 23 | public static final AttributeDescriptor RESPONSE = new AttributeDescriptor<>("response", ValueType.TEXT) 24 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Response from device command")); 25 | 26 | 27 | public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("gear", null, TeltonikaConfigurationAsset.class); 28 | 29 | protected TeltonikaConfigurationAsset(){ 30 | 31 | } 32 | 33 | public TeltonikaConfigurationAsset(String name){ 34 | super(name); 35 | super.setLocation(new GeoJSONPoint(0,0,0)); 36 | super.setNotes(""); 37 | } 38 | 39 | public TeltonikaConfigurationAsset setWhitelist(String[] whitelist) { 40 | getAttributes().getOrCreate(WHITELIST).setValue(whitelist); 41 | return this; 42 | } 43 | public TeltonikaConfigurationAsset setEnabled(boolean enabled) { 44 | getAttributes().getOrCreate(ENABLED).setValue(enabled); 45 | return this; 46 | } 47 | 48 | public TeltonikaConfigurationAsset setCheckForImei(boolean enabled) { 49 | getAttributes().getOrCreate(CHECK_FOR_IMEI).setValue(enabled); 50 | return this; 51 | } 52 | 53 | public TeltonikaConfigurationAsset setDefaultModelNumber(String value) { 54 | getAttributes().getOrCreate(DEFAULT_MODEL_NUMBER).setValue(value); 55 | return this; 56 | } 57 | 58 | public TeltonikaConfigurationAsset setCommandTopic(String value){ 59 | getAttributes().getOrCreate(COMMAND).setValue(value); 60 | return this; 61 | } 62 | public TeltonikaConfigurationAsset setResponseTopic(String value){ 63 | getAttributes().getOrCreate(RESPONSE).setValue(value); 64 | return this; 65 | } 66 | 67 | public TeltonikaConfigurationAsset setStorePayloads(Boolean value) { 68 | getAttributes().getOrCreate(STORE_PAYLOADS).setValue(value); 69 | return this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CustomAsset.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | package org.openremote.model.custom; 21 | 22 | import org.openremote.model.asset.Asset; 23 | import org.openremote.model.asset.AssetDescriptor; 24 | import org.openremote.model.value.AttributeDescriptor; 25 | import org.openremote.model.value.ValueDescriptor; 26 | 27 | import jakarta.persistence.Entity; 28 | import java.util.Optional; 29 | 30 | /** 31 | * This is an example of a custom {@link Asset} type; this must be registered via an 32 | * {@link org.openremote.model.AssetModelProvider} and must conform to the following requirements: 33 | * 34 | *
    35 | *
  • Must have {@link Entity} annotation 36 | *
  • Optionally add {@link org.openremote.model.value.ValueDescriptor}s 37 | *
  • Optionally add {@link org.openremote.model.value.MetaItemDescriptor}s 38 | *
  • Optionally add {@link org.openremote.model.value.AttributeDescriptor}s 39 | *
  • Must have a public static final {@link org.openremote.model.asset.AssetDescriptor} 40 | *
  • Must have a protected no args constructor (for hydrators i.e. JPA/Jackson) 41 | *
  • For a given {@link Asset} type only one {@link org.openremote.model.asset.AssetDescriptor} can exist 42 | *
  • {@link org.openremote.model.value.AttributeDescriptor}s that override a super class descriptor cannot change the 43 | * value type; just the formatting etc. 44 | *
  • {@link org.openremote.model.value.MetaItemDescriptor}s names must be unique 45 | *
  • {@link org.openremote.model.value.ValueDescriptor}s names must be unique 46 | *
47 | */ 48 | @Entity 49 | public class CustomAsset extends Asset { 50 | 51 | public enum CustomValueType { 52 | ONE, 53 | TWO, 54 | THREE 55 | } 56 | 57 | public static final ValueDescriptor CUSTOM_VALUE_TYPE_VALUE_DESCRIPTOR = new ValueDescriptor<>("customValueType", CustomValueType.class); 58 | 59 | public static final AttributeDescriptor CUSTOM_VALUE_TYPE_ATTRIBUTE_DESCRIPTOR = new AttributeDescriptor<>("customAttribute", CUSTOM_VALUE_TYPE_VALUE_DESCRIPTOR); 60 | 61 | public static final AssetDescriptor CUSTOM_ASSET_ASSET_DESCRIPTOR = new AssetDescriptor<>("brightness-auto", "00aaaa", CustomAsset.class); 62 | 63 | public Optional getCustomAttribute() { 64 | return getAttributes().getValue(CUSTOM_VALUE_TYPE_ATTRIBUTE_DESCRIPTOR); 65 | } 66 | 67 | public CustomAsset setCustomAttribute(CustomValueType value) { 68 | getAttributes().getOrCreate(CUSTOM_VALUE_TYPE_ATTRIBUTE_DESCRIPTOR).setValue(value); 69 | return this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenRemote Fleet Management Integration v1 2 | 3 | ![CI/CD](https://github.com/openremote/fleet-management/workflows/CI/CD/badge.svg) 4 | [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/) 5 | 6 | This repository contains the OpenRemote custom project that contains full support for fleet management features, like 7 | location tracking and session tracking, and also the industry-first **complete, automatic data recognition** from any 8 | Teltonika Telematics device model. 9 | 10 | Please look at the wiki for the tutorial on how to set up your own fleet management system, and the Developer Guide to 11 | understand the inner workings of the fleet management implementation. 12 | 13 | ## Quickstart 14 | 15 | An online demo will be made available to the public shortly, but you can still run the OpenRemote fleet management 16 | implementation locally using Docker: 17 | 18 | The quickest way to get your own environment with full access is to make use of our docker images (both `amd64` and 19 | `arm64` are supported). 20 | 1. Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop) installed (v18+). 21 | 2. Download the docker compose file: 22 | [OpenRemote Stack](https://raw.githubusercontent.com/openremote/fleet-management/master/docker-compose.yml) (Right click 'Save link as...') 23 | 3. In a terminal `cd` to where you just saved the compose file and then run: 24 | ``` 25 | docker-compose -p fleet-management up -d 26 | ``` 27 | If all goes well then you should now be able to access the OpenRemote Manager UI at [https://localhost](https://localhost). 28 | You will need to accept the self-signed certificate, see [here](https://www.technipages.com/google-chrome-bypass-your-connection-is-not-private-message) for details how to do this in Chrome 29 | (similar for other browsers). 30 | 31 | To configure the devices and OpenRemote to properly communicate, check the tutorial and quickstart guides in the wiki. 32 | 33 | ### Login credentials 34 | Username: admin 35 | Password: secret 36 | 37 | ### Changing host and/or port 38 | The URL you use to access the system is important, the default is configured as `https://localhost` if you are using a VM or want to run on a different port then you will need to set the `OR_HOSTNAME` and `OR_SSL_PORT` environment variables, so if for example you will be accessing using `https://192.168.1.1:8443` then use the following startup command: 39 | 40 | BASH: 41 | ``` 42 | OR_HOSTNAME=192.168.1.1 OR_SSL_PORT=8443 docker-compose -p fleet-management up -d 43 | ``` 44 | or 45 | 46 | CMD: 47 | ``` 48 | cmd /C "set OR_HOSTNAME=192.168.1.1 && set OR_SSL_PORT=8443 && docker-compose -p fleet-management up -d" 49 | ``` 50 | 51 | 52 | # Custom Project Format 53 | 54 | To create the OpenRemote fleet management integration, a new custom project was created using [OpenRemote's custom-project template](https://github.com/openremote/custom-project). To view the changes of files between the original custom-project repository and the current state of the repository, press [here]( https://github.com/openremote/fleet-management/compare/668ae6fdfb20eeae5977ad62b655bf3fb3d58cdd...main). In this way, you can see the files that have been added since the creation of this repository. 55 | 56 | This repository uses the [feature/fleet-management](https://github.com/openremote/openremote/tree/feature/fleet-management) branch of the main OpenRemote repository as its core, specifically for adding more UI-related features. If the UI features are not something that interest you, you're encouraged to change the submodule to use the `master` OpenRemote branch. 57 | 58 | 59 | # Support and Community 60 | 61 | For support, comments, questions, and concerns, please head to [OpenRemote's forum](https://forum.openremote.io/) and post any questions here. Issues and Pull-Requests created here could be ignored, so if they do, please contact us through the forum. 62 | -------------------------------------------------------------------------------- /model-util/src/main/java/AggregatedApiClient.java: -------------------------------------------------------------------------------- 1 | import cz.habarta.typescript.generator.Settings; 2 | import cz.habarta.typescript.generator.emitter.EmitterExtension; 3 | import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; 4 | import cz.habarta.typescript.generator.emitter.TsBeanModel; 5 | import cz.habarta.typescript.generator.emitter.TsModel; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Creates a wrapper class around all of the clients the generator creates so that they can easily be exported as a single 12 | * entity. 13 | */ 14 | public class AggregatedApiClient extends EmitterExtension { 15 | 16 | private static final String fieldFormatString = "protected %1$s : Axios%2$sClient;"; 17 | private static final String ctorFormatString = "this.%1$s = new Axios%2$sClient(baseURL, axiosInstance);"; 18 | private static final String getter1FormatString = "get %1$s() : Axios%1$sClient {"; 19 | private static final String getter2FormatString = "return this.%1$s;"; 20 | 21 | private static class FieldAndGetter { 22 | String field; 23 | String getter; 24 | 25 | public FieldAndGetter(String field, String getter) { 26 | this.field = field; 27 | this.getter = getter; 28 | } 29 | } 30 | 31 | @Override 32 | public EmitterExtensionFeatures getFeatures() { 33 | final EmitterExtensionFeatures features = new EmitterExtensionFeatures(); 34 | features.generatesRuntimeCode = true; 35 | return features; 36 | } 37 | 38 | 39 | @Override 40 | public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) { 41 | 42 | writer.writeIndentedLine("export class ApiClient {"); 43 | writer.writeIndentedLine(""); 44 | List clients = model.getBeans().stream() 45 | .filter(TsBeanModel::isJaxrsApplicationClientBean) 46 | .collect(Collectors.toList()); 47 | 48 | List fieldsAndGetters = clients.stream().map(this::getOutputs).collect(Collectors.toList()); 49 | 50 | fieldsAndGetters.forEach(fieldAndGetter -> this.emitField(writer, fieldAndGetter)); 51 | 52 | writer.writeIndentedLine(""); 53 | writer.writeIndentedLine("constructor(baseURL: string, axiosInstance: Axios.AxiosInstance = axios.create()) {"); 54 | 55 | fieldsAndGetters.forEach(fieldAndGetter -> this.emitCtor(writer, fieldAndGetter)); 56 | 57 | writer.writeIndentedLine("this._assetResource = new AxiosAssetResourceClient(baseURL, axiosInstance);"); 58 | writer.writeIndentedLine("}"); 59 | writer.writeIndentedLine(""); 60 | 61 | fieldsAndGetters.forEach(fieldAndGetter -> this.emitGetter(writer, fieldAndGetter)); 62 | 63 | writer.writeIndentedLine("}"); 64 | } 65 | 66 | private void emitField(Writer writer, FieldAndGetter fieldAndGetter) { 67 | writer.writeIndentedLine(String.format(fieldFormatString, fieldAndGetter.field, fieldAndGetter.getter)); 68 | } 69 | 70 | private void emitCtor(Writer writer, FieldAndGetter fieldAndGetter) { 71 | writer.writeIndentedLine(String.format(ctorFormatString, fieldAndGetter.field, fieldAndGetter.getter)); 72 | } 73 | 74 | private void emitGetter(Writer writer, FieldAndGetter fieldAndGetter) { 75 | writer.writeIndentedLine(String.format(getter1FormatString, fieldAndGetter.getter)); 76 | writer.writeIndentedLine(String.format(getter2FormatString, fieldAndGetter.field)); 77 | writer.writeIndentedLine("}"); 78 | } 79 | 80 | private FieldAndGetter getOutputs(TsBeanModel client) { 81 | String getterName = client.getName().getSimpleName().substring(0, client.getName().getSimpleName().length()-6); 82 | String fieldName = "_" + Character.toLowerCase(getterName.charAt(0)) + getterName.substring(1); 83 | return new FieldAndGetter(fieldName, getterName); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ui/app/custom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenRemote Custom App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 65 | 66 | 67 | 68 | 69 | 75 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/TeltonikaModelConfigurationAsset.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.teltonika; 2 | 3 | import jakarta.persistence.Entity; 4 | import org.openremote.model.asset.Asset; 5 | import org.openremote.model.asset.AssetDescriptor; 6 | import org.openremote.model.attribute.Attribute; 7 | import org.openremote.model.attribute.MetaItem; 8 | import org.openremote.model.attribute.MetaMap; 9 | import org.openremote.model.custom.CustomValueTypes; 10 | import org.openremote.model.geo.GeoJSONPoint; 11 | import org.openremote.model.value.AttributeDescriptor; 12 | import org.openremote.model.value.MetaItemType; 13 | import org.openremote.model.value.ValueType; 14 | 15 | import java.util.Arrays; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.stream.Collectors; 19 | 20 | import static org.openremote.model.value.MetaItemType.*; 21 | 22 | @Entity 23 | public class TeltonikaModelConfigurationAsset extends Asset { 24 | public static final AttributeDescriptor MODEL_NUMBER = new AttributeDescriptor<>("modelNumber", ValueType.TEXT); 25 | public static final AttributeDescriptor PARAMETER_DATA = new AttributeDescriptor<>("TeltonikaParameterData", CustomValueTypes.TELTONIKA_PARAMETER.asArray()); 26 | 27 | public static final AttributeDescriptor PARAMETER_MAP = new AttributeDescriptor<>("TeltonikaParameterMap", CustomValueTypes.TELTONIKA_PARAMETER_MAP) 28 | .withMeta(new MetaMap(Map.of(MetaItemType.READ_ONLY.getName(), new MetaItem<>(MetaItemType.READ_ONLY, true)))); 29 | public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("switch", null, TeltonikaModelConfigurationAsset.class); 30 | 31 | protected TeltonikaModelConfigurationAsset(){} 32 | 33 | public TeltonikaModelConfigurationAsset(String name){ 34 | super(name); 35 | super.setLocation(new GeoJSONPoint(0,0,0)); 36 | super.setNotes(""); 37 | } 38 | 39 | public TeltonikaModelConfigurationAsset setModelNumber(String name) { 40 | getAttributes().getOrCreate(MODEL_NUMBER).setValue(name); 41 | return this; 42 | } 43 | 44 | public TeltonikaModelConfigurationAsset setParameterData(TeltonikaParameter[] data) { 45 | //Get TeltonikaParameter array, cast to Map with parameter ID as key, save to PARAMETER_MAP, remove from PARAMETER_DATA 46 | getAttributes().getOrCreate(PARAMETER_DATA).setValue(data); 47 | CustomValueTypes.TeltonikaParameterMap map = Arrays.stream(data).collect(Collectors.toMap( 48 | TeltonikaParameter::getPropertyId, // Key Mapper 49 | param -> param, // Value Mapper 50 | (existing, replacement) -> replacement, // Merge Function 51 | CustomValueTypes.TeltonikaParameterMap::new 52 | )); 53 | 54 | getAttributes().getOrCreate(PARAMETER_MAP).setValue(map); 55 | 56 | // 57 | 58 | 59 | 60 | return this; 61 | } 62 | 63 | public Optional getModelNumber(){ 64 | return getAttributes().getValue(MODEL); 65 | } 66 | 67 | public CustomValueTypes.TeltonikaParameterMap getParameterMap() { 68 | Optional> map = getAttributes().get(PARAMETER_MAP); 69 | 70 | return map.flatMap(Attribute::getValue) 71 | .orElse(new CustomValueTypes.TeltonikaParameterMap()); // or provide a default value other than null, if appropriate 72 | } 73 | 74 | public static MetaMap getPayloadAttributeMeta(String label){ 75 | MetaMap map = new MetaMap(); 76 | 77 | map.addAll( 78 | new MetaItem<>(STORE_DATA_POINTS, true), 79 | new MetaItem<>(RULE_STATE, true), 80 | new MetaItem<>(READ_ONLY, true), 81 | new MetaItem<>(LABEL, label) 82 | ); 83 | 84 | return map; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/custom/CarAsset.java: -------------------------------------------------------------------------------- 1 | package org.openremote.model.custom; 2 | 3 | import jakarta.persistence.Entity; 4 | import org.openremote.model.Constants; 5 | import org.openremote.model.asset.AssetDescriptor; 6 | import org.openremote.model.attribute.MetaItem; 7 | import org.openremote.model.attribute.MetaMap; 8 | import org.openremote.model.teltonika.TeltonikaModelConfigurationAsset; 9 | import org.openremote.model.value.AttributeDescriptor; 10 | import org.openremote.model.value.MetaItemType; 11 | import org.openremote.model.value.ValueType; 12 | import org.openremote.model.value.impl.ColourRGB; 13 | import scala.collection.immutable.Stream; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | /** 19 | * CarAsset is an extension of the VehicleAsset class, specifically intended for the fleet management use case of OpenRemote. 20 | * It is used as the class for the car asset type that should work using this integration. 21 | * 22 | * This Asset Type is used as both an example and as a viable use-case for the OpenRemote Fleet Telematics integration 23 | * of OpenRemote with Teltonika Telematics. 24 | * 25 | * It contains the correct, user-fillable metadata, while also containing some specific attributes that are widely used 26 | * in the fleet management use case. 27 | * 28 | * In this situation, the user has two options; either extend the Vehicle asset as this class, or extend this asset type, 29 | * to use the attributes that are included in it. 30 | * */ 31 | @Entity 32 | public class CarAsset extends VehicleAsset{ 33 | public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("car", null, CarAsset.class); 34 | 35 | // Vehicle meta-data 36 | public static final AttributeDescriptor COLOR = new AttributeDescriptor<>("color", ValueType.COLOUR_RGB).withOptional(true); 37 | public static final AttributeDescriptor MODEL_YEAR = new AttributeDescriptor<>("modelYear", ValueType.INTEGER).withOptional(true) 38 | .withUnits(Constants.UNITS_YEAR); 39 | public static final AttributeDescriptor LICENSE_PLATE = new AttributeDescriptor<>("licensePlate", ValueType.TEXT).withOptional(true); 40 | 41 | //Ignition 42 | public static final AttributeDescriptor IGNITION_ON = new AttributeDescriptor<>("239", ValueType.BOOLEAN) 43 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Ignition status")); 44 | 45 | //Movement 46 | public static final AttributeDescriptor MOVEMENT = new AttributeDescriptor<>("240", ValueType.BOOLEAN) 47 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Movement status")); 48 | 49 | //odometer 50 | public static final AttributeDescriptor ODOMETER = new AttributeDescriptor<>("16", ValueType.NUMBER) 51 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Odometer")) 52 | .withUnits(Constants.UNITS_METRE); 53 | 54 | 55 | // All the permanent ones (pr, alt, ang, sat, sp, evt) 56 | 57 | public static final AttributeDescriptor EVENT_ATTR_NAME = new AttributeDescriptor<>("evt", ValueType.NUMBER).withOptional(true) 58 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Event triggered by")); 59 | public static final AttributeDescriptor ALTITUDE = new AttributeDescriptor<>("alt", ValueType.NUMBER).withOptional(true) 60 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Altitude")) 61 | .withUnits(Constants.UNITS_METRE); 62 | public static final AttributeDescriptor SATELLITES = new AttributeDescriptor<>("sat", ValueType.NUMBER).withOptional(true) 63 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Number of satellites in use")); 64 | public static final AttributeDescriptor SPEED = new AttributeDescriptor<>("sp", ValueType.NUMBER) 65 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Speed")) 66 | .withUnits(Constants.UNITS_KILO, Constants.UNITS_METRE, Constants.UNITS_PER, Constants.UNITS_HOUR); 67 | public static final AttributeDescriptor PRIORITY = new AttributeDescriptor<>("pr", ValueType.NUMBER).withOptional(true) 68 | .withMeta(TeltonikaModelConfigurationAsset.getPayloadAttributeMeta("Payload priority (0-2)")); 69 | 70 | 71 | 72 | //Hydration 73 | protected CarAsset() { 74 | } 75 | 76 | public CarAsset(String name) { 77 | super(name); 78 | } 79 | public Optional getModelYear() { 80 | return getAttributes().getValue(MODEL_YEAR); 81 | } 82 | public Optional getColor() { 83 | return getAttributes().getValue(COLOR); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /model-util/src/main/java/CustomAggregatedApiClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | import cz.habarta.typescript.generator.Settings; 21 | import cz.habarta.typescript.generator.emitter.EmitterExtension; 22 | import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; 23 | import cz.habarta.typescript.generator.emitter.TsBeanModel; 24 | import cz.habarta.typescript.generator.emitter.TsModel; 25 | 26 | import java.util.List; 27 | import java.util.stream.Collectors; 28 | 29 | /** 30 | * Creates a wrapper class around all of the clients the generator creates so that they can easily be exported as a single 31 | * entity; this is specifically for custom projects and creates a reference to the main rest client. 32 | */ 33 | public class CustomAggregatedApiClient extends EmitterExtension { 34 | 35 | private static final String fieldFormatString = "protected %1$s! : Axios%2$sClient;"; 36 | private static final String getter1FormatString = "get %1$s() : Axios%1$sClient {"; 37 | private static final String getter2FormatString = "if (this.%1$s != null) return this.%1$s;"; 38 | private static final String getter3FormatString = "this.%1$s = new Axios%2$sClient(rest.baseUrl, rest.axiosInstance);"; 39 | private static final String getter4FormatString = "return this.%1$s;"; 40 | 41 | private static class FieldAndGetter { 42 | String field; 43 | String getter; 44 | 45 | public FieldAndGetter(String field, String getter) { 46 | this.field = field; 47 | this.getter = getter; 48 | } 49 | } 50 | 51 | @Override 52 | public EmitterExtensionFeatures getFeatures() { 53 | final EmitterExtensionFeatures features = new EmitterExtensionFeatures(); 54 | features.generatesRuntimeCode = true; 55 | return features; 56 | } 57 | 58 | 59 | @Override 60 | public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) { 61 | 62 | writer.writeIndentedLine("import rest from \"@openremote/rest\";"); 63 | writer.writeIndentedLine(""); 64 | writer.writeIndentedLine("export class ApiClient {"); 65 | writer.writeIndentedLine(""); 66 | List clients = model.getBeans().stream() 67 | .filter(TsBeanModel::isJaxrsApplicationClientBean) 68 | .collect(Collectors.toList()); 69 | 70 | List fieldsAndGetters = clients.stream().map(this::getOutputs).collect(Collectors.toList()); 71 | 72 | fieldsAndGetters.forEach(fieldAndGetter -> this.emitField(writer, fieldAndGetter)); 73 | 74 | writer.writeIndentedLine(""); 75 | 76 | fieldsAndGetters.forEach(fieldAndGetter -> this.emitGetter(writer, fieldAndGetter)); 77 | 78 | writer.writeIndentedLine("}"); 79 | } 80 | 81 | private void emitField(Writer writer, FieldAndGetter fieldAndGetter) { 82 | writer.writeIndentedLine(String.format(fieldFormatString, fieldAndGetter.field, fieldAndGetter.getter)); 83 | } 84 | 85 | private void emitGetter(Writer writer, FieldAndGetter fieldAndGetter) { 86 | writer.writeIndentedLine(String.format(getter1FormatString, fieldAndGetter.getter)); 87 | writer.writeIndentedLine(String.format(getter2FormatString, fieldAndGetter.field)); 88 | writer.writeIndentedLine(String.format(getter3FormatString, fieldAndGetter.field, fieldAndGetter.getter)); 89 | writer.writeIndentedLine(String.format(getter4FormatString, fieldAndGetter.field)); 90 | writer.writeIndentedLine("}"); 91 | } 92 | 93 | private FieldAndGetter getOutputs(TsBeanModel client) { 94 | String getterName = client.getName().getSimpleName().substring(0, client.getName().getSimpleName().length()-6); 95 | String fieldName = "_" + Character.toLowerCase(getterName.charAt(0)) + getterName.substring(1); 96 | return new FieldAndGetter(fieldName, getterName); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/helpers/TeltonikaConfigurationFactory.java: -------------------------------------------------------------------------------- 1 | package telematics.teltonika.helpers; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.openremote.container.timer.TimerService; 6 | import org.openremote.manager.asset.AssetStorageService; 7 | import org.openremote.model.query.AssetQuery; 8 | import org.openremote.model.query.filter.ParentPredicate; 9 | import org.openremote.model.query.filter.RealmPredicate; 10 | import org.openremote.model.syslog.SyslogCategory; 11 | import org.openremote.model.teltonika.TeltonikaConfigurationAsset; 12 | import org.openremote.model.teltonika.TeltonikaModelConfigurationAsset; 13 | import org.openremote.model.teltonika.TeltonikaParameter; 14 | import telematics.teltonika.TeltonikaConfiguration; 15 | 16 | import java.util.Date; 17 | import java.util.List; 18 | import java.util.logging.Logger; 19 | 20 | import static org.openremote.model.syslog.SyslogCategory.API; 21 | 22 | public class TeltonikaConfigurationFactory { 23 | 24 | private static final Logger LOG = SyslogCategory.getLogger(API, TeltonikaConfigurationFactory.class); 25 | 26 | public static TeltonikaConfiguration createConfiguration(AssetStorageService assetStorageService, TimerService timerService, String fileLocation) { 27 | try{ 28 | return getConfig(assetStorageService, timerService, fileLocation); 29 | } catch (Exception e) { 30 | if(e instanceof IndexOutOfBoundsException) { 31 | LOG.severe("More than 1 Master Teltonika configurations found! Shutting down."); 32 | throw e; 33 | } else if (e instanceof IllegalStateException) { 34 | LOG.severe("No Master Teltonika configuration found! Creating default configuration."); 35 | initializeConfigurationAssets(fileLocation, assetStorageService); 36 | return getConfig(assetStorageService, timerService, fileLocation); 37 | } 38 | throw e; 39 | } 40 | 41 | } 42 | 43 | private static TeltonikaConfiguration getConfig(AssetStorageService assetStorageService, TimerService timerService, String fileLocation) { 44 | List masterAssets = assetStorageService.findAll( 45 | new AssetQuery() 46 | .types(TeltonikaConfigurationAsset.class) 47 | .realm(new RealmPredicate("master")) 48 | 49 | ) 50 | .stream() 51 | .map(asset -> (TeltonikaConfigurationAsset) asset) 52 | .toList(); 53 | 54 | 55 | if (masterAssets.size() > 1) { 56 | throw new IndexOutOfBoundsException("More than 1 Master Teltonika configurations found! Shutting down."); 57 | } 58 | if (masterAssets.isEmpty()) { 59 | throw new IllegalStateException("No Master Teltonika configuration found! You need to create a new default configuration."); 60 | 61 | } 62 | 63 | List modelAssets = assetStorageService.findAll( 64 | new AssetQuery() 65 | .types(TeltonikaModelConfigurationAsset.class) 66 | .realm(new RealmPredicate("master")) 67 | .parents(new ParentPredicate(masterAssets.get(0).getId())) 68 | ) 69 | .stream() 70 | .map(asset -> (TeltonikaModelConfigurationAsset) asset) 71 | .toList(); 72 | 73 | return new TeltonikaConfiguration(masterAssets.get(0), modelAssets, new Date(timerService.getCurrentTimeMillis())); 74 | 75 | } 76 | 77 | private static void initializeConfigurationAssets(String fileLocation, AssetStorageService assetStorageService) { 78 | // Create initial configuration 79 | TeltonikaConfigurationAsset rootConfig = new TeltonikaConfigurationAsset("Teltonika Device Configuration"); 80 | TeltonikaModelConfigurationAsset fmc003 = new TeltonikaModelConfigurationAsset("FMC003"); 81 | 82 | rootConfig.setEnabled(true); 83 | rootConfig.setCheckForImei(false); 84 | rootConfig.setDefaultModelNumber("FMC003"); 85 | rootConfig.setCommandTopic("sendToDevice"); 86 | rootConfig.setResponseTopic("response"); 87 | rootConfig.setStorePayloads(false); 88 | 89 | fmc003.setModelNumber("FMC003"); 90 | ObjectMapper mapper = new ObjectMapper(); 91 | try { 92 | TeltonikaParameter[] params = mapper.readValue(fileLocation, TeltonikaParameter[].class); 93 | fmc003.setParameterData(params); 94 | 95 | } catch (JsonProcessingException e) { 96 | throw new RuntimeException("Could not parse Teltonika Parameter JSON file"); 97 | } 98 | 99 | rootConfig.setRealm("master"); 100 | fmc003.setRealm("master"); 101 | 102 | rootConfig = assetStorageService.merge(rootConfig); 103 | 104 | fmc003.setParent(rootConfig); 105 | 106 | assetStorageService.merge(fmc003); 107 | 108 | } 109 | 110 | public static void refreshInstance(AssetStorageService assetStorageService, TimerService timerService, String fileLocation) { 111 | createConfiguration(assetStorageService, timerService, fileLocation); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /docker-compose-test.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for deploying the custom stack; uses deployment-data named volume 4 | # to expose customisations to the manager and keycloak images. To run this profile you need to specify the following 5 | # environment variables: 6 | # 7 | # OR_ADMIN_PASSWORD - Initial admin user password 8 | # OR_HOSTNAME - FQDN hostname of where this instance will be exposed (localhost, IP address or public domain) 9 | # DEPLOYMENT_VERSION - Tag to use for deployment image (must match the tag used when building the deployment image) 10 | # 11 | # Please see openremote/profile/deploy.yml for configuration details for each service. 12 | # 13 | # To perform updates, build code and prepare Docker images: 14 | # 15 | # ./gradlew clean installDist 16 | # 17 | # Then recreate deployment image: 18 | # 19 | # DEPLOYMENT_VERSION=$(git rev-parse --short HEAD) 20 | # MANAGER_VERSION=$(cd openremote; git rev-parse --short HEAD; cd ..) 21 | # docker build -t openremote/manager:$MANAGER_VERSION ./openremote/manager/build/install/manager/ 22 | # docker build -t openremote/custom-deployment:$DEPLOYMENT_VERSION ./deployment/build/ 23 | # docker-compose -p custom down 24 | # docker volume rm custom_deployment-data 25 | # Do the following volume rm command if you want a clean install (wipe all existing data) 26 | # docker volume rm custom_postgresql-data 27 | # OR_ADMIN_PASSWORD=secret OR_HOSTNAME=my.domain.com docker-compose -p custom up -d 28 | # 29 | # All data is kept in volumes. Create a backup of the volumes to preserve data. 30 | # 31 | volumes: 32 | proxy-data: 33 | deployment-data: 34 | postgresql-data: 35 | manager-data: 36 | 37 | # Add an NFS volume to the stack 38 | # efs-data: 39 | # driver: local 40 | # driver_opts: 41 | # type: nfs 42 | # o: "addr=${EFS_DNS?DNS must be set to mount NFS volume},rw,nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport" 43 | # device: ":/" 44 | 45 | 46 | # Re-enable when you figure it out 47 | #x-logging: &awslogs 48 | # logging: 49 | # driver: awslogs 50 | # options: 51 | # awslogs-region: ${AWS_REGION:-eu-west-1} 52 | # awslogs-group: ${OR_HOSTNAME} 53 | # awslogs-create-group: 'true' 54 | # tag: "{{.Name}}/{{.ID}}" 55 | 56 | services: 57 | 58 | # This service will only populate an empty volume on startup and then exit. 59 | # If the volume already contains data, it exits immediately. 60 | deployment: 61 | image: pankalog/fleet-deployment:${DEPLOYMENT_VERSION:-latest} 62 | volumes: 63 | - deployment-data:/deployment 64 | 65 | proxy: 66 | image: openremote/proxy:${PROXY_VERSION:-latest} 67 | restart: always 68 | depends_on: 69 | manager: 70 | condition: service_healthy 71 | ports: 72 | - "80:80" 73 | - "443:443" 74 | - "8883:8883" 75 | volumes: 76 | - proxy-data:/deployment 77 | - deployment-data:/data 78 | environment: 79 | LE_EMAIL: ${OR_EMAIL_ADMIN} 80 | DOMAINNAME: ${OR_HOSTNAME:-localhost} 81 | DOMAINNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 82 | # USE A CUSTOM PROXY CONFIG - COPY FROM https://github.com/openremote/proxy/blob/main/haproxy.cfg 83 | #HAPROXY_CONFIG: '/data/proxy/haproxy.cfg' 84 | # <<: *awslogs 85 | 86 | postgresql: 87 | image: openremote/postgresql:${POSTGRESQL_VERSION:-latest} 88 | restart: always 89 | volumes: 90 | - postgresql-data:/var/lib/postgresql/data 91 | - manager-data:/storage 92 | # <<: *awslogs 93 | 94 | keycloak: 95 | image: openremote/keycloak:${KEYCLOAK_VERSION:-latest} 96 | restart: always 97 | depends_on: 98 | postgresql: 99 | condition: service_healthy 100 | volumes: 101 | - deployment-data:/deployment 102 | environment: 103 | KEYCLOAK_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD:-secret} 104 | KC_HOSTNAME: ${OR_HOSTNAME:-localhost} 105 | KC_HOSTNAME_PORT: ${OR_SSL_PORT:--1} 106 | # <<: *awslogs 107 | 108 | manager: 109 | image: pankalog/fleet-management:${MANAGER_VERSION:-latest} 110 | restart: always 111 | depends_on: 112 | keycloak: 113 | condition: service_healthy 114 | volumes: 115 | - manager-data:/storage 116 | - deployment-data:/deployment 117 | # Map data should be accessed from a volume mount 118 | # 1). Host filesystem - /deployment.local:/deployment.local 119 | # 2) NFS/EFS network mount - efs-data:/efs 120 | environment: 121 | # Here are some typical environment variables you want to set 122 | # see openremote/profile/deploy.yml for details 123 | OR_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD:-secret} 124 | OR_SETUP_TYPE: # Typical values to support are staging and production 125 | OR_SETUP_RUN_ON_RESTART: 126 | OR_EMAIL_HOST: 127 | OR_EMAIL_USER: 128 | OR_EMAIL_PASSWORD: 129 | OR_EMAIL_X_HEADERS: 130 | OR_EMAIL_FROM: 131 | OR_EMAIL_ADMIN: 132 | OR_HOSTNAME: ${OR_HOSTNAME:-localhost} 133 | OR_ADDITIONAL_HOSTNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 134 | OR_SSL_PORT: ${OR_SSL_PORT:--1} 135 | OR_DEV_MODE: ${OR_DEV_MODE:-false} 136 | OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 137 | #OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 138 | # <<: *awslogs 139 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for deploying the custom stack; uses deployment-data named volume 4 | # to expose customisations to the manager and keycloak images. To run this profile you need to specify the following 5 | # environment variables: 6 | # 7 | # OR_ADMIN_PASSWORD - Initial admin user password 8 | # OR_HOSTNAME - FQDN hostname of where this instance will be exposed (localhost, IP address or public domain) 9 | # DEPLOYMENT_VERSION - Tag to use for deployment image (must match the tag used when building the deployment image) 10 | # 11 | # Please see openremote/profile/deploy.yml for configuration details for each service. 12 | # 13 | # To perform updates, build code and prepare Docker images: 14 | # 15 | # ./gradlew clean installDist 16 | # 17 | # Then recreate deployment image: 18 | # 19 | # DEPLOYMENT_VERSION=$(git rev-parse --short HEAD) 20 | # MANAGER_VERSION=$(cd openremote; git rev-parse --short HEAD; cd ..) 21 | # docker build -t openremote/manager:$MANAGER_VERSION ./openremote/manager/build/install/manager/ 22 | # docker build -t openremote/custom-deployment:$DEPLOYMENT_VERSION ./deployment/build/ 23 | # docker-compose -p custom down 24 | # docker volume rm custom_deployment-data 25 | # Do the following volume rm command if you want a clean install (wipe all existing data) 26 | # docker volume rm custom_postgresql-data 27 | # OR_ADMIN_PASSWORD=secret OR_HOSTNAME=my.domain.com docker-compose -p custom up -d 28 | # 29 | # All data is kept in volumes. Create a backup of the volumes to preserve data. 30 | # 31 | volumes: 32 | proxy-data: 33 | deployment-data: 34 | postgresql-data: 35 | manager-data: 36 | 37 | # Add an NFS volume to the stack 38 | # efs-data: 39 | # driver: local 40 | # driver_opts: 41 | # type: nfs 42 | # o: "addr=${EFS_DNS?DNS must be set to mount NFS volume},rw,nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport" 43 | # device: ":/" 44 | 45 | # x-logging: &awslogs 46 | # logging: 47 | # driver: awslogs 48 | # options: 49 | # awslogs-region: ${AWS_REGION:-eu-west-1} 50 | # awslogs-group: ${OR_HOSTNAME} 51 | # awslogs-create-group: 'true' 52 | # tag: "{{.Name}}/{{.ID}}" 53 | 54 | services: 55 | 56 | # This service will only populate an empty volume on startup and then exit. 57 | # If the volume already contains data, it exits immediately. 58 | deployment: 59 | image: pankalog/fleet-deployment:${DEPLOYMENT_VERSION:-latest} 60 | volumes: 61 | - deployment-data:/deployment 62 | 63 | proxy: 64 | image: openremote/proxy:${PROXY_VERSION:-latest} 65 | restart: always 66 | depends_on: 67 | manager: 68 | condition: service_healthy 69 | ports: 70 | - "80:80" 71 | - "443:443" 72 | - "8883:8883" 73 | volumes: 74 | - proxy-data:/deployment 75 | - deployment-data:/data 76 | environment: 77 | LE_EMAIL: ${OR_EMAIL_ADMIN} 78 | DOMAINNAME: ${OR_HOSTNAME?OR_HOSTNAME must be set} 79 | DOMAINNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 80 | # USE A CUSTOM PROXY CONFIG - COPY FROM https://github.com/openremote/proxy/blob/main/haproxy.cfg 81 | #HAPROXY_CONFIG: '/data/proxy/haproxy.cfg' 82 | # <<: *awslogs 83 | 84 | postgresql: 85 | image: openremote/postgresql:${POSTGRESQL_VERSION:-latest} 86 | restart: always 87 | volumes: 88 | - postgresql-data:/var/lib/postgresql/data 89 | - manager-data:/storage 90 | # <<: *awslogs 91 | 92 | keycloak: 93 | image: openremote/keycloak:${KEYCLOAK_VERSION:-latest} 94 | restart: always 95 | depends_on: 96 | postgresql: 97 | condition: service_healthy 98 | volumes: 99 | - deployment-data:/deployment 100 | environment: 101 | KEYCLOAK_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD:?OR_ADMIN_PASSWORD must be set} 102 | KC_HOSTNAME: ${OR_HOSTNAME:-localhost} 103 | KC_HOSTNAME_PORT: ${OR_SSL_PORT:--1} 104 | # <<: *awslogs 105 | 106 | manager: 107 | image: openremote/manager:${MANAGER_VERSION:-latest} 108 | restart: always 109 | depends_on: 110 | keycloak: 111 | condition: service_healthy 112 | volumes: 113 | - manager-data:/storage 114 | - deployment-data:/deployment 115 | # Map data should be accessed from a volume mount 116 | # 1). Host filesystem - /deployment.local:/deployment.local 117 | # 2) NFS/EFS network mount - efs-data:/efs 118 | environment: 119 | # Here are some typical environment variables you want to set 120 | # see openremote/profile/deploy.yml for details 121 | OR_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD?OR_ADMIN_PASSWORD must be set} 122 | OR_SETUP_TYPE: # Typical values to support are staging and production 123 | OR_SETUP_RUN_ON_RESTART: 124 | OR_EMAIL_HOST: 125 | OR_EMAIL_USER: 126 | OR_EMAIL_PASSWORD: 127 | OR_EMAIL_X_HEADERS: 128 | OR_EMAIL_FROM: 129 | OR_EMAIL_ADMIN: 130 | OR_HOSTNAME: ${OR_HOSTNAME?OR_HOSTNAME must be set} 131 | OR_ADDITIONAL_HOSTNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 132 | OR_SSL_PORT: ${OR_SSL_PORT:--1} 133 | OR_DEV_MODE: ${OR_DEV_MODE:-false} 134 | OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 135 | #OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 136 | # <<: *awslogs 137 | -------------------------------------------------------------------------------- /profile/prod_cicd.yml: -------------------------------------------------------------------------------- 1 | # OpenRemote v3 2 | # 3 | # Profile for deploying the custom stack; uses deployment-data named volume 4 | # to expose customisations to the manager and keycloak images. To run this profile you need to specify the following 5 | # environment variables: 6 | # 7 | # OR_ADMIN_PASSWORD - Initial admin user password 8 | # OR_HOSTNAME - FQDN hostname of where this instance will be exposed (localhost, IP address or public domain) 9 | # DEPLOYMENT_VERSION - Tag to use for deployment image (must match the tag used when building the deployment image) 10 | # 11 | # Please see openremote/profile/deploy.yml for configuration details for each service. 12 | # 13 | # To perform updates, build code and prepare Docker images: 14 | # 15 | # ./gradlew clean installDist 16 | # 17 | # Then recreate deployment image: 18 | # 19 | # DEPLOYMENT_VERSION=$(git rev-parse --short HEAD) 20 | # MANAGER_VERSION=$(cd openremote; git rev-parse --short HEAD; cd ..) 21 | # docker build -t openremote/manager:$MANAGER_VERSION ./openremote/manager/build/install/manager/ 22 | # docker build -t openremote/custom-deployment:$DEPLOYMENT_VERSION ./deployment/build/ 23 | # docker-compose -p custom down 24 | # docker volume rm custom_deployment-data 25 | # Do the following volume rm command if you want a clean install (wipe all existing data) 26 | # docker volume rm custom_postgresql-data 27 | # OR_ADMIN_PASSWORD=secret OR_HOSTNAME=my.domain.com docker-compose -p custom up -d 28 | # 29 | # All data is kept in volumes. Create a backup of the volumes to preserve data. 30 | # 31 | volumes: 32 | proxy-data: 33 | deployment-data: 34 | postgresql-data: 35 | manager-data: 36 | 37 | # Add an NFS volume to the stack 38 | efs-data: 39 | driver: local 40 | driver_opts: 41 | type: nfs 42 | o: "addr=${EFS_DNS?DNS must be set to mount NFS volume},rw,nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport" 43 | device: ":/" 44 | 45 | x-logging: &awslogs 46 | logging: 47 | driver: awslogs 48 | options: 49 | awslogs-region: ${AWS_REGION:-eu-west-1} 50 | awslogs-group: ${OR_HOSTNAME} 51 | awslogs-create-group: 'true' 52 | tag: "{{.Name}}/{{.ID}}" 53 | 54 | services: 55 | 56 | # This service will only populate an empty volume on startup and then exit. 57 | # If the volume already contains data, it exits immediately. 58 | deployment: 59 | image: openremote/deployment:${DEPLOYMENT_VERSION?DEPLOYMENT_VERSION must be set} 60 | volumes: 61 | - deployment-data:/deployment 62 | 63 | proxy: 64 | image: openremote/proxy:${PROXY_VERSION:-latest} 65 | restart: always 66 | depends_on: 67 | manager: 68 | condition: service_healthy 69 | ports: 70 | - "80:80" 71 | - "443:443" 72 | - "8883:8883" 73 | volumes: 74 | - proxy-data:/deployment 75 | - deployment-data:/data 76 | environment: 77 | LE_EMAIL: ${OR_EMAIL_ADMIN} 78 | DOMAINNAME: ${OR_HOSTNAME?OR_HOSTNAME must be set} 79 | DOMAINNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 80 | # USE A CUSTOM PROXY CONFIG - COPY FROM https://github.com/openremote/proxy/blob/main/haproxy.cfg 81 | #HAPROXY_CONFIG: '/data/proxy/haproxy.cfg' 82 | <<: *awslogs 83 | 84 | postgresql: 85 | image: openremote/postgresql:${POSTGRESQL_VERSION:-latest} 86 | restart: always 87 | volumes: 88 | - postgresql-data:/var/lib/postgresql/data 89 | - manager-data:/storage 90 | <<: *awslogs 91 | 92 | keycloak: 93 | image: openremote/keycloak:${KEYCLOAK_VERSION:-latest} 94 | restart: always 95 | depends_on: 96 | postgresql: 97 | condition: service_healthy 98 | volumes: 99 | - deployment-data:/deployment 100 | environment: 101 | KEYCLOAK_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD:?OR_ADMIN_PASSWORD must be set} 102 | KC_HOSTNAME: ${OR_HOSTNAME:-localhost} 103 | KC_HOSTNAME_PORT: ${OR_SSL_PORT:--1} 104 | <<: *awslogs 105 | 106 | manager: 107 | image: openremote/manager:${MANAGER_VERSION:-latest} 108 | restart: always 109 | depends_on: 110 | keycloak: 111 | condition: service_healthy 112 | volumes: 113 | - manager-data:/storage 114 | - deployment-data:/deployment 115 | # Map data should be accessed from a volume mount 116 | # 1). Host filesystem - /deployment.local:/deployment.local 117 | # 2) NFS/EFS network mount - efs-data:/efs 118 | - efs-data:/efs 119 | environment: 120 | # Here are some typical environment variables you want to set 121 | # see openremote/profile/deploy.yml for details 122 | OR_ADMIN_PASSWORD: ${OR_ADMIN_PASSWORD?OR_ADMIN_PASSWORD must be set} 123 | OR_SETUP_TYPE: # Typical values to support are staging and production 124 | OR_SETUP_RUN_ON_RESTART: 125 | OR_EMAIL_HOST: 126 | OR_EMAIL_USER: 127 | OR_EMAIL_PASSWORD: 128 | OR_EMAIL_X_HEADERS: 129 | OR_EMAIL_FROM: 130 | OR_EMAIL_ADMIN: 131 | OR_HOSTNAME: ${OR_HOSTNAME?OR_HOSTNAME must be set} 132 | OR_ADDITIONAL_HOSTNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} 133 | OR_SSL_PORT: ${OR_SSL_PORT:--1} 134 | OR_DEV_MODE: ${OR_DEV_MODE:-false} 135 | OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 136 | #OR_MAP_TILES_PATH: '/efs/europe.mbtiles' 137 | <<: *awslogs 138 | -------------------------------------------------------------------------------- /model-util/src/main/java/CustomTypeProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import com.fasterxml.jackson.databind.JsonNode; 22 | import cz.habarta.typescript.generator.TsType; 23 | import cz.habarta.typescript.generator.TypeProcessor; 24 | import cz.habarta.typescript.generator.util.Utils; 25 | import org.openremote.model.util.TsIgnore; 26 | import org.openremote.model.util.TsIgnoreTypeParams; 27 | 28 | import java.lang.reflect.ParameterizedType; 29 | import java.lang.reflect.Type; 30 | import java.util.*; 31 | 32 | /** 33 | * Does some custom processing for our specific model and fixes any anomalies in the plugin itself: 34 | *
    35 | *
  • Ignore types/super types annotated with {@link TsIgnore} 36 | *
  • Will ignore types with a super type in the "com.fasterxml.jackson" package excluding those implementing {@link com.fasterxml.jackson.databind.JsonNode}
  • 37 | *
  • Removes some or all type params from classes annotated with {@link TsIgnoreTypeParams} 38 | *
  • Special processing for AssetModelInfo meta item value descriptors as JsonSerialize extension doesn't support @JsonSerialize(contentConverter=...) 39 | *
40 | */ 41 | public class CustomTypeProcessor implements TypeProcessor { 42 | 43 | public static final String JACKSON_PACKAGE = "com.fasterxml.jackson"; 44 | 45 | @Override 46 | public Result processType(Type javaType, Context context) { 47 | Class rawClass = Utils.getRawClassOrNull(javaType); 48 | 49 | if (rawClass == null) { 50 | return null; 51 | } 52 | 53 | // Look through type hierarchy 54 | while (rawClass != null && rawClass != Object.class) { 55 | // Look for TsIgnore annotation 56 | if (rawClass.getAnnotation(TsIgnore.class) != null) { 57 | return new Result(TsType.Any); 58 | } 59 | 60 | if (JsonNode.class.isAssignableFrom(rawClass)) { 61 | return null; 62 | } 63 | 64 | if (rawClass.getName().startsWith(JACKSON_PACKAGE)) { 65 | return new Result(TsType.Any); 66 | } 67 | rawClass = rawClass.getSuperclass(); 68 | } 69 | 70 | rawClass = Utils.getRawClassOrNull(javaType); 71 | 72 | if (javaType instanceof ParameterizedType parameterizedType) { 73 | 74 | // Map -> {[id: string]: unknown} to prevent arrays matching 75 | if (rawClass == Map.class && Utils.getRawClassOrNull(parameterizedType.getActualTypeArguments()[0]) == String.class && Utils.getRawClassOrNull(parameterizedType.getActualTypeArguments()[1]) == Object.class) { 76 | return new Result(new TsType.IndexedArrayType(TsType.String, TsType.Unknown)); 77 | } 78 | 79 | // Exclude type params 80 | TsIgnoreTypeParams ignoreTypeParams = rawClass.getAnnotation(TsIgnoreTypeParams.class); 81 | 82 | if (ignoreTypeParams != null) { 83 | 84 | if (ignoreTypeParams.paramIndexes().length == 0) { 85 | // Ignore all 86 | return new Result(new TsType.BasicType(rawClass.getSimpleName())); 87 | } 88 | 89 | // Ignore specified 90 | List removeIndexes = Arrays.stream(ignoreTypeParams.paramIndexes()).boxed().toList(); 91 | 92 | if (parameterizedType.getRawType() instanceof final Class javaClass) { 93 | final List> discoveredClasses = new ArrayList<>(); 94 | discoveredClasses.add(javaClass); 95 | final Type[] typeArguments = parameterizedType.getActualTypeArguments(); 96 | final List tsTypeArguments = new ArrayList<>(); 97 | for (int i=0; i. 19 | */ 20 | 21 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 22 | import com.fasterxml.jackson.databind.util.Converter; 23 | import cz.habarta.typescript.generator.compiler.ModelCompiler; 24 | import cz.habarta.typescript.generator.compiler.ModelTransformer; 25 | import cz.habarta.typescript.generator.compiler.SymbolTable; 26 | import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; 27 | import cz.habarta.typescript.generator.parser.BeanModel; 28 | import cz.habarta.typescript.generator.parser.Model; 29 | import cz.habarta.typescript.generator.parser.PropertyModel; 30 | 31 | import java.lang.reflect.Field; 32 | import java.lang.reflect.Member; 33 | import java.lang.reflect.Method; 34 | import java.util.Arrays; 35 | import java.util.List; 36 | import java.util.concurrent.atomic.AtomicBoolean; 37 | import java.util.stream.Collectors; 38 | 39 | /** 40 | * Extension for applying {@link JsonSerialize} annotation. Supports: 41 | *
    42 | *
  • {@link JsonSerialize#as}
  • 43 | *
  • {@link JsonSerialize#converter}
  • 44 | *
45 | */ 46 | @SuppressWarnings("deprecation") 47 | public class JsonSerializeExtension extends cz.habarta.typescript.generator.Extension { 48 | 49 | @Override 50 | public EmitterExtensionFeatures getFeatures() { 51 | return new EmitterExtensionFeatures(); 52 | } 53 | 54 | @Override 55 | public List getTransformers() { 56 | return Arrays.asList( 57 | new cz.habarta.typescript.generator.Extension.TransformerDefinition(ModelCompiler.TransformationPhase.BeforeTsModel, new ModelTransformer() { 58 | @Override 59 | public Model transformModel(SymbolTable symbolTable, Model model) { 60 | // Look for @JsonSerialize annotation and modify the property type accordingly 61 | List beans = model.getBeans(); 62 | beans.replaceAll(bean -> { 63 | AtomicBoolean modified = new AtomicBoolean(false); 64 | 65 | List properties = bean.getProperties().stream().map(p -> { 66 | Member member = p.getOriginalMember(); 67 | JsonSerialize jsonSerialize = null; 68 | 69 | if (member instanceof Field) { 70 | Field field = (Field)member; 71 | jsonSerialize = field.getAnnotation(JsonSerialize.class); 72 | } else if (member instanceof Method) { 73 | Method method = (Method)member; 74 | jsonSerialize = method.getAnnotation(JsonSerialize.class); 75 | } 76 | 77 | if (jsonSerialize != null) { 78 | // TODO: Add support for other options 79 | if (jsonSerialize.as() != Void.class) { 80 | modified.set(true); 81 | return new PropertyModel(p.getName(), jsonSerialize.as(), p.isOptional(), p.getAccess(), p.getOriginalMember(), p.getPullProperties(), p.getContext(), p.getComments()); 82 | } 83 | if (jsonSerialize.converter() != Converter.None.class) { 84 | // Type info is not accessible with reflection so instantiate the converter 85 | Method convertMethod = Arrays.stream(jsonSerialize.converter().getMethods()).filter(m -> m.getName().equals("convert")).findFirst().orElse(null); 86 | if (convertMethod != null) { 87 | modified.set(true); 88 | return new PropertyModel(p.getName(), convertMethod.getGenericReturnType(), p.isOptional(), p.getAccess(), p.getOriginalMember(), p.getPullProperties(), p.getContext(), p.getComments()); 89 | } 90 | } 91 | } 92 | 93 | return p; 94 | }).collect(Collectors.toList()); 95 | 96 | if (modified.get()) { 97 | return new BeanModel(bean.getOrigin(), bean.getParent(), bean.getTaggedUnionClasses(), bean.getDiscriminantProperty(), bean.getDiscriminantLiteral(), bean.getInterfaces(), properties, bean.getComments()); 98 | } 99 | 100 | return bean; 101 | }); 102 | 103 | return model; 104 | } 105 | }) 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /model-util/src/main/java/CustomExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import cz.habarta.typescript.generator.Extension; 22 | import cz.habarta.typescript.generator.TsType; 23 | import cz.habarta.typescript.generator.compiler.ModelCompiler; 24 | import cz.habarta.typescript.generator.compiler.ModelTransformer; 25 | import cz.habarta.typescript.generator.compiler.SymbolTable; 26 | import cz.habarta.typescript.generator.compiler.TsModelTransformer; 27 | import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; 28 | import cz.habarta.typescript.generator.emitter.TsBeanModel; 29 | import cz.habarta.typescript.generator.emitter.TsPropertyModel; 30 | import cz.habarta.typescript.generator.parser.BeanModel; 31 | import cz.habarta.typescript.generator.parser.Model; 32 | import org.openremote.model.asset.AssetTypeInfo; 33 | import org.openremote.model.attribute.AttributeEvent; 34 | import org.openremote.model.provisioning.X509ProvisioningConfig; 35 | import org.openremote.model.util.TsIgnoreTypeParams; 36 | 37 | import java.util.Arrays; 38 | import java.util.Collections; 39 | import java.util.List; 40 | 41 | /** 42 | * Does some custom processing for our specific model and fixes any anomalies in the plugin itself: 43 | *
    44 | *
  • Removes some or all type params from classes annotated with {@link TsIgnoreTypeParams} 45 | *
  • Special processing for AssetModelInfo meta item value descriptors as JsonSerialize extension doesn't support @JsonSerialize(contentConverter=...) 46 | *
47 | */ 48 | public class CustomExtension extends Extension { 49 | 50 | @Override 51 | public EmitterExtensionFeatures getFeatures() { 52 | return new EmitterExtensionFeatures(); 53 | } 54 | 55 | @Override 56 | public List getTransformers() { 57 | return Arrays.asList( 58 | // This is a hack to fix breaking change with latest version of this plugin 59 | new TransformerDefinition(ModelCompiler.TransformationPhase.AfterDeclarationSorting, (TsModelTransformer) (context, model) -> { 60 | TsBeanModel provBean = model.getBean(X509ProvisioningConfig.class); 61 | if (provBean != null) { 62 | provBean.getExtendsList().remove(0); 63 | provBean.getExtendsList().add(provBean.getParent()); 64 | } 65 | return model; 66 | }), 67 | new TransformerDefinition(ModelCompiler.TransformationPhase.BeforeEnums, (TsModelTransformer) (context, model) -> { 68 | 69 | TsBeanModel assetTypeInfoBean = model.getBean(AssetTypeInfo.class); 70 | if (assetTypeInfoBean != null) { 71 | assetTypeInfoBean.getProperties().replaceAll(p -> p.getName().equals("metaItemDescriptors") || p.getName().equals("valueDescriptors") ? new TsPropertyModel(p.getName(), new TsType.BasicArrayType(TsType.String), p.modifiers, p.ownProperty, p.comments) : p); 72 | } 73 | 74 | // Remove the type parameter - this works in conjunction with the CustomTypeProcessor which replaces 75 | // field references 76 | model.getBeans().replaceAll(bean -> { 77 | 78 | if (bean.getOrigin() != null && bean.getOrigin().getAnnotation(TsIgnoreTypeParams.class) != null) { 79 | if (bean.getTypeParameters() != null) { 80 | TsIgnoreTypeParams ignoreTypeParams = bean.getOrigin().getAnnotation(TsIgnoreTypeParams.class); 81 | if (ignoreTypeParams.paramIndexes().length == 0) { 82 | bean.getTypeParameters().clear(); 83 | } else { 84 | Arrays.stream(ignoreTypeParams.paramIndexes()) 85 | .boxed() 86 | .sorted(Collections.reverseOrder()) 87 | .forEach(index -> bean.getTypeParameters().remove(index.intValue())); 88 | } 89 | } 90 | } 91 | 92 | return bean; 93 | }); 94 | 95 | return model; 96 | }), 97 | new cz.habarta.typescript.generator.Extension.TransformerDefinition(ModelCompiler.TransformationPhase.BeforeTsModel, new ModelTransformer() { 98 | @Override 99 | public Model transformModel(SymbolTable symbolTable, Model model) { 100 | 101 | // Remove attribute state from attribute event (can't do this with annotations) 102 | BeanModel attrEventBean = model.getBean(AttributeEvent.class); 103 | if (attrEventBean != null) { 104 | attrEventBean.getProperties().removeIf(pm -> pm.getName().equals("attributeState")); 105 | } 106 | return model; 107 | } 108 | }) 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /model/src/main/java/org/openremote/model/teltonika/TeltonikaParameter.java: -------------------------------------------------------------------------------- 1 | 2 | package org.openremote.model.teltonika; 3 | 4 | import java.io.Serializable; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | 9 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 10 | import com.fasterxml.jackson.annotation.JsonAnySetter; 11 | import com.fasterxml.jackson.annotation.JsonIgnore; 12 | import com.fasterxml.jackson.annotation.JsonInclude; 13 | import com.fasterxml.jackson.annotation.JsonProperty; 14 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 15 | 16 | @JsonInclude(JsonInclude.Include.NON_NULL) 17 | @JsonPropertyOrder({ 18 | "propertyIdInAvlPacket", 19 | "propertyName", 20 | "bytes", 21 | "type", 22 | "min", 23 | "max", 24 | "multiplier", 25 | "units", 26 | "description", 27 | "hwSupport", 28 | "parameterGroup" 29 | }) 30 | //TODO: Create interface for this class called TelematicsDeviceParameter 31 | public class TeltonikaParameter implements Serializable { 32 | 33 | @JsonProperty("propertyIdInAvlPacket") 34 | public Integer propertyId; 35 | @JsonProperty("propertyName") 36 | public String propertyName; 37 | @JsonProperty("bytes") 38 | public String bytes; 39 | @JsonProperty("type") 40 | public String type; 41 | @JsonProperty("min") 42 | public String min; 43 | @JsonProperty("max") 44 | public String max; 45 | @JsonProperty("multiplier") 46 | public String multiplier; 47 | @JsonProperty("units") 48 | public String units; 49 | @JsonProperty("description") 50 | public String description; 51 | @JsonProperty("hwSupport") 52 | public String hwSupport; 53 | @JsonProperty("parameterGroup") 54 | public String parameterGroup; 55 | 56 | @JsonIgnore 57 | private Map additionalProperties = new LinkedHashMap(); 58 | 59 | public TeltonikaParameter(Integer propertyId, String propertyName, String bytes, String type, String min, String max, String multiplier, String units, String description, String hwSupport, String parameterGroup) { 60 | this.propertyId = propertyId; 61 | this.propertyName = propertyName; 62 | this.bytes = bytes; 63 | this.type = type; 64 | this.min = min; 65 | this.max = max; 66 | this.multiplier = multiplier; 67 | this.units = units; 68 | this.description = description; 69 | this.hwSupport = hwSupport; 70 | this.parameterGroup = parameterGroup; 71 | } 72 | 73 | public TeltonikaParameter(){} 74 | 75 | 76 | @JsonAnyGetter 77 | public Map getAdditionalProperties() { 78 | return this.additionalProperties; 79 | } 80 | 81 | @JsonAnySetter 82 | public void setAdditionalProperty(String name, Object value) { 83 | this.additionalProperties.put(name, value); 84 | } 85 | 86 | 87 | 88 | @Override 89 | public String toString() { 90 | StringBuilder sb = new StringBuilder(); 91 | sb.append(TeltonikaParameter.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('['); 92 | sb.append("propertyIdInAvlPacket"); 93 | sb.append('='); 94 | sb.append(((this.propertyId == null)?"":this.propertyId)); 95 | sb.append(','); 96 | sb.append("propertyName"); 97 | sb.append('='); 98 | sb.append(((this.propertyName == null)?"":this.propertyName)); 99 | sb.append(','); 100 | sb.append("bytes"); 101 | sb.append('='); 102 | sb.append(((this.bytes == null)?"":this.bytes)); 103 | sb.append(','); 104 | sb.append("type"); 105 | sb.append('='); 106 | sb.append(((this.type == null)?"":this.type)); 107 | sb.append(','); 108 | sb.append("min"); 109 | sb.append('='); 110 | sb.append(((this.min == null)?"":this.min)); 111 | sb.append(','); 112 | sb.append("max"); 113 | sb.append('='); 114 | sb.append(((this.max == null)?"":this.max)); 115 | sb.append(','); 116 | sb.append("multiplier"); 117 | sb.append('='); 118 | sb.append(((this.multiplier == null)?"":this.multiplier)); 119 | sb.append(','); 120 | sb.append("units"); 121 | sb.append('='); 122 | sb.append(((this.units == null)?"":this.units)); 123 | sb.append(','); 124 | sb.append("description"); 125 | sb.append('='); 126 | sb.append(((this.description == null)?"":this.description)); 127 | sb.append(','); 128 | sb.append("hwSupport"); 129 | sb.append('='); 130 | sb.append(((this.hwSupport == null)?"":this.hwSupport)); 131 | sb.append(','); 132 | sb.append("parameterGroup"); 133 | sb.append('='); 134 | sb.append(((this.parameterGroup == null)?"":this.parameterGroup)); 135 | sb.append(','); 136 | sb.append("additionalProperties"); 137 | sb.append('='); 138 | sb.append(((this.additionalProperties == null)?"":this.additionalProperties)); 139 | sb.append(','); 140 | if (sb.charAt((sb.length()- 1)) == ',') { 141 | sb.setCharAt((sb.length()- 1), ']'); 142 | } else { 143 | sb.append(']'); 144 | } 145 | return sb.toString(); 146 | } 147 | 148 | public Integer getPropertyId(){ 149 | return this.propertyId; 150 | } 151 | 152 | @Override 153 | public boolean equals(Object param) { 154 | 155 | if(param.getClass() == this.getClass()){ 156 | return Objects.equals(((TeltonikaParameter)param).getPropertyId(), this.getPropertyId()); 157 | } return false; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /model-util/src/main/java/org/openremote/model/util/AssetModelInfoExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, OpenRemote Inc. 3 | * 4 | * See the CONTRIBUTORS.txt file in the distribution for a 5 | * full listing of individual contributors. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package org.openremote.model.util; 22 | 23 | import cz.habarta.typescript.generator.Extension; 24 | import cz.habarta.typescript.generator.Settings; 25 | import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; 26 | import cz.habarta.typescript.generator.emitter.TsModel; 27 | import org.openremote.agent.protocol.AgentModelProvider; 28 | import org.openremote.model.Constants; 29 | import org.openremote.model.rules.Ruleset; 30 | import org.openremote.model.value.MetaItemDescriptor; 31 | 32 | import java.util.Arrays; 33 | import java.util.HashMap; 34 | import java.util.Locale; 35 | import java.util.Map; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | 38 | 39 | /** 40 | * Outputs enums for well known Asset types, attribute names and meta item names 41 | * Outputs fields from {@link org.openremote.model.Constants} that begin with "UNITS" as a UNITS enum 42 | */ 43 | public class AssetModelInfoExtension extends Extension { 44 | 45 | public AssetModelInfoExtension() { 46 | // Ensure the asset model is initialised 47 | ValueUtil.initialise(null); 48 | // Service loader doesn't seem to work so manually load the agent model provider 49 | ValueUtil.getModelProviders().add(new AgentModelProvider()); 50 | ValueUtil.doInitialise(); 51 | } 52 | 53 | @Override 54 | public EmitterExtensionFeatures getFeatures() { 55 | final EmitterExtensionFeatures features = new EmitterExtensionFeatures(); 56 | features.generatesRuntimeCode = true; 57 | return features; 58 | } 59 | 60 | @SuppressWarnings("rawtypes") 61 | @Override 62 | public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) { 63 | 64 | Map assetMap = new HashMap<>(); 65 | Map otherMap = new HashMap<>(); 66 | 67 | Arrays.stream(ValueUtil.getAssetInfos(null)).forEach(assetModelInfo -> { 68 | String assetDescriptorName = assetModelInfo.getAssetDescriptor().getName(); 69 | assetMap.put(assetDescriptorName.toUpperCase(Locale.ROOT), assetDescriptorName); 70 | 71 | // Store attributes 72 | assetModelInfo.getAttributeDescriptors().values().forEach(attributeDescriptor -> { 73 | String attributeName = attributeDescriptor.getName(); 74 | otherMap.put(attributeName.toUpperCase(Locale.ROOT), attributeName); 75 | }); 76 | }); 77 | 78 | emitEnum(writer, "WellknownAssets", assetMap); 79 | 80 | writer.writeIndentedLine(""); 81 | emitEnum(writer, "WellknownAttributes", otherMap); 82 | 83 | otherMap.clear(); 84 | ValueUtil.getMetaItemDescriptors().values().forEach(metaItemDescriptor -> { 85 | String metaName = metaItemDescriptor.getName(); 86 | otherMap.put(metaName.toUpperCase(Locale.ROOT), metaName); 87 | }); 88 | writer.writeIndentedLine(""); 89 | emitEnum(writer, "WellknownMetaItems", otherMap); 90 | 91 | otherMap.clear(); 92 | ValueUtil.getValueDescriptors().values().forEach(valueDescriptor -> { 93 | String valueTypeName = valueDescriptor.getName(); 94 | otherMap.put(valueTypeName.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", ""), valueTypeName); 95 | }); 96 | writer.writeIndentedLine(""); 97 | emitEnum(writer, "WellknownValueTypes", otherMap); 98 | 99 | otherMap.clear(); 100 | Arrays.stream(Constants.class.getFields()).filter(f -> f.getName().startsWith("UNITS_")).forEach(unitField -> { 101 | String unitName = unitField.getName().substring(6); 102 | try { 103 | otherMap.put(unitName.toUpperCase(Locale.ROOT), (String)unitField.get(null)); 104 | } catch (IllegalAccessException e) { 105 | e.printStackTrace(); 106 | } 107 | }); 108 | writer.writeIndentedLine(""); 109 | emitEnum(writer, "WellknownUnitTypes", otherMap); 110 | 111 | otherMap.clear(); 112 | Arrays.stream(Ruleset.class.getFields()).filter(f -> f.getType() == MetaItemDescriptor.class).forEach(rulesetMeta -> { 113 | try { 114 | MetaItemDescriptor metaItemDescriptor = (MetaItemDescriptor) rulesetMeta.get(null); 115 | otherMap.put(metaItemDescriptor.getName().toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", ""), metaItemDescriptor.getName()); 116 | } catch (IllegalAccessException e) { 117 | e.printStackTrace(); 118 | } 119 | }); 120 | writer.writeIndentedLine(""); 121 | emitEnum(writer, "WellknownRulesetMetaItems", otherMap); 122 | } 123 | 124 | protected void emitEnum(Writer writer, String name, Map values) { 125 | writer.writeIndentedLine("export const enum " + name + " {"); 126 | int last = values.size(); 127 | AtomicInteger counter = new AtomicInteger(1); 128 | values.forEach((itemName, itemValue) -> { 129 | writer.writeIndentedLine(" " + itemName + " = \"" + itemValue + "\"" + (last != counter.getAndIncrement() ? "," : "")); 130 | }); 131 | writer.writeIndentedLine("}"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /manager/src/main/java/telematics/teltonika/TeltonikaConfiguration.java: -------------------------------------------------------------------------------- 1 | package telematics.teltonika; 2 | 3 | import org.openremote.manager.asset.AssetStorageService; 4 | import org.openremote.model.attribute.Attribute; 5 | import org.openremote.model.custom.CustomValueTypes; 6 | import org.openremote.model.query.AssetQuery; 7 | import org.openremote.model.query.filter.ParentPredicate; 8 | import org.openremote.model.query.filter.RealmPredicate; 9 | import org.openremote.model.teltonika.TeltonikaConfigurationAsset; 10 | import org.openremote.model.teltonika.TeltonikaModelConfigurationAsset; 11 | 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.NoSuchElementException; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | *

20 | * {@code TeltonikaConfiguration} is a class that holds the configuration for Teltonika devices. 21 | *

22 | *
23 | *

24 | * It employs the Singleton pattern to ensure that only one instance of the class is created. 25 | * We employ the Singleton pattern because retrieving the configuration data is a costly process, which requires a lot 26 | * of requests to the database to retrieve the attributes. 27 | * We want to ensure that we only retrieve the configuration data once and then use it throughout the application. 28 | *

29 | *
30 | *

31 | * The class also employs the Factory pattern in {@code TeltonikaConfigurationFactory} to create the configuration 32 | * object. The factory method is used to create the configuration object and to ensure that only one instance of the 33 | * configuration object is created. 34 | * The factory class does help with managing the instance and creating it, but also helps with keeping it up to date. 35 | * To ensure that the configuration object is up to date, the factory class uses a timestamp to check if the 36 | * configuration has been used for up to a minute. When a minute has elapsed 37 | * (editable using {@code CONFIGURATION_UPDATE_INTERVAL}), it refreshes the instance of the configuration object. 38 | *

39 | *
40 | *

41 | * We want to be sure that the configuration is always up to date, even when the user is configuring the asset. 42 | * To do so, we employ a sufficient-enough version of the observer pattern on the TeltonikaConfigurationFactory. 43 | * When the AttributeEvent handler for configuration asset data detects a change, it triggers a refresh of the 44 | * instance. 45 | *

46 | *
47 | *

48 | * The TeltonikaConfiguration object is also using a builder pattern to make it easier to instantiate and use the 49 | * configuration data. 50 | *

51 | *
52 | *

53 | * A Teltonika configuration consists of multiple assets; one {@code TeltonikaMasterConfigurationAsset} and multiple 54 | * {@code TeltonikaModelConfigurationAssets}. All assets are only retrieved from the {@code master} realm. 55 | * The {@code TeltonikaMasterConfigurationAsset} holds the configuration for the totality of the implementation, with 56 | * settings like payload storage etc., while the {@code TeltonikaModelConfigurationAssets} hold the configuration for 57 | * specific Teltonika device models. They specifically contain the model number and the parameters to be used 58 | * for parsing of the payloads received. 59 | *

60 | */ 61 | public class TeltonikaConfiguration { 62 | 63 | private static TeltonikaConfiguration instance = null; 64 | 65 | public static TeltonikaConfiguration getInstance() throws NullPointerException { 66 | if (instance == null) throw new NullPointerException("TeltonikaConfiguration instance is null"); 67 | //if the update time has elapsed, request update to configuration 68 | if (latestUpdateTimestamp == null || 69 | (new Date().getTime() - latestUpdateTimestamp.getTime()) > CONFIGURATION_UPDATE_INTERVAL 70 | ) { 71 | throw new NullPointerException("TeltonikaConfiguration instance needs to be refreshed."); 72 | } 73 | return instance; 74 | } 75 | 76 | public static void setInstance(TeltonikaConfiguration instance) { 77 | TeltonikaConfiguration.instance = instance; 78 | } 79 | 80 | private static Date latestUpdateTimestamp = null; 81 | 82 | private static final int CONFIGURATION_UPDATE_INTERVAL = 1000 * 60 * 1; // 1 minute 83 | 84 | TeltonikaConfigurationAsset masterAsset; 85 | 86 | public TeltonikaConfigurationAsset getMasterAsset() { 87 | return masterAsset; 88 | } 89 | /** 90 | * Maps Model Number to Map of parameters: {@code [{"FMCOO3": {}...]}} 91 | */ 92 | HashMap defaultParameterMap = new HashMap<>(); 93 | 94 | private List modelAssets; 95 | 96 | 97 | 98 | public TeltonikaConfiguration(TeltonikaConfigurationAsset master, List models, Date date){ 99 | 100 | 101 | if (master == null) return; 102 | if (models.isEmpty()) return; 103 | 104 | masterAsset = master; 105 | 106 | defaultParameterMap = models.stream().collect(Collectors.toMap( 107 | val ->val.getAttributes().get(TeltonikaModelConfigurationAsset.MODEL_NUMBER).get().getValue().get(), // Key Mapper 108 | TeltonikaModelConfigurationAsset::getParameterMap, // Value Mapper 109 | (existing, replacement) -> replacement, // Merge Function 110 | HashMap::new 111 | )); 112 | 113 | modelAssets = models; 114 | latestUpdateTimestamp = date; 115 | 116 | } 117 | 118 | public List getModelAssets() { 119 | return modelAssets; 120 | } 121 | 122 | public TeltonikaModelConfigurationAsset getModelAsset(String modelNumber) throws NoSuchElementException { 123 | return modelAssets.stream() 124 | .filter( 125 | val -> val.getAttributes().get(TeltonikaModelConfigurationAsset.MODEL_NUMBER).get().getValue().get().equals(modelNumber) 126 | ) 127 | .findFirst().orElseThrow(NoSuchElementException::new); 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return "TeltonikaConfiguration{" + 133 | "masterAsset=" + masterAsset + 134 | ", parameterMap=" + defaultParameterMap + 135 | ", modelAssets=" + modelAssets + 136 | '}'; 137 | } 138 | 139 | public List getChildModelIDs(){ 140 | return modelAssets.stream().map(TeltonikaModelConfigurationAsset::getId).collect(Collectors.toList()); 141 | } 142 | 143 | public Boolean getEnabled(){ 144 | return masterAsset.getAttribute(TeltonikaConfigurationAsset.ENABLED).get().getValue().get(); 145 | } 146 | 147 | public boolean getCheckForImei() { 148 | return masterAsset.getAttribute(TeltonikaConfigurationAsset.CHECK_FOR_IMEI).get().getValue().get(); 149 | } 150 | 151 | public String getDefaultModelNumber() { 152 | return masterAsset.getAttribute(TeltonikaConfigurationAsset.DEFAULT_MODEL_NUMBER).get().getValue().get(); 153 | } 154 | 155 | public HashMap getModelParameterMap(String modelNumber) { 156 | return defaultParameterMap; 157 | } 158 | 159 | public Attribute getCommandAttribute(){ 160 | return getMasterAsset().getAttribute(TeltonikaConfigurationAsset.COMMAND).get(); 161 | } 162 | public Attribute getResponseAttribute(){ 163 | return getMasterAsset().getAttribute(TeltonikaConfigurationAsset.RESPONSE).get(); 164 | } 165 | 166 | public Attribute getStorePayloads(){ 167 | return getMasterAsset().getAttribute(TeltonikaConfigurationAsset.STORE_PAYLOADS).get(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /ui/component/model/src/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AgentDescriptor, 3 | AssetDescriptor, 4 | AssetTypeInfo, Attribute, 5 | AttributeDescriptor, 6 | MetaItemDescriptor, 7 | ValueDescriptor, ValueDescriptorHolder, ValueHolder, WellknownAssets, WellknownValueTypes 8 | } from "./index"; 9 | 10 | export class AssetModelUtil { 11 | 12 | static _assetTypeInfos: AssetTypeInfo[] = []; 13 | static _metaItemDescriptors: MetaItemDescriptor[] = []; 14 | static _valueDescriptors: ValueDescriptor[] = []; 15 | 16 | public static getAssetDescriptors(): AssetDescriptor[] { 17 | return AssetModelUtil._assetTypeInfos.map(info => info.assetDescriptor as AgentDescriptor); 18 | } 19 | 20 | public static getMetaItemDescriptors(): MetaItemDescriptor[] { 21 | return [...this._metaItemDescriptors]; 22 | } 23 | 24 | public static getValueDescriptors(): ValueDescriptor[] { 25 | return [...this._valueDescriptors]; 26 | } 27 | 28 | public static getAssetTypeInfos(): AssetTypeInfo[] { 29 | return [...this._assetTypeInfos]; 30 | } 31 | 32 | public static getAssetTypeInfo(type: string | AssetDescriptor | AssetTypeInfo): AssetTypeInfo | undefined { 33 | if (!type) { 34 | return; 35 | } 36 | 37 | if ((type as AssetTypeInfo).assetDescriptor) { 38 | return type as AssetTypeInfo; 39 | } 40 | 41 | if (typeof(type) !== "string") { 42 | type = (type as AssetDescriptor).name!; 43 | } 44 | 45 | return this._assetTypeInfos.find((assetTypeInfo) => { 46 | return assetTypeInfo.assetDescriptor!.name === type; 47 | }); 48 | } 49 | 50 | public static getAssetDescriptor(type?: string | AssetDescriptor | AssetTypeInfo): AssetDescriptor | undefined { 51 | if (!type) { 52 | return; 53 | } 54 | 55 | if ((type as AssetTypeInfo).assetDescriptor) { 56 | return (type as AssetTypeInfo).assetDescriptor; 57 | } 58 | 59 | if (typeof(type) !== "string") { 60 | return type as AssetDescriptor; 61 | } 62 | 63 | const match = this._assetTypeInfos.find((assetTypeInfo) => { 64 | return assetTypeInfo.assetDescriptor!.name === type; 65 | }); 66 | return match ? match.assetDescriptor : undefined; 67 | } 68 | 69 | public static getAttributeDescriptor(attributeName: string, assetTypeOrDescriptor: string | AssetDescriptor | AssetTypeInfo): AttributeDescriptor | undefined { 70 | if (!attributeName) { 71 | return; 72 | } 73 | 74 | const assetTypeInfo = this.getAssetTypeInfo(assetTypeOrDescriptor || WellknownAssets.THINGASSET); 75 | 76 | if (!assetTypeInfo || !assetTypeInfo.attributeDescriptors) { 77 | return; 78 | } 79 | 80 | return assetTypeInfo.attributeDescriptors.find((attributeDescriptor) => attributeDescriptor.name === attributeName); 81 | } 82 | 83 | public static getValueDescriptor(name?: string): ValueDescriptor | undefined { 84 | if (!name) { 85 | return; 86 | } 87 | 88 | // If name ends with [] then it's an array value type so lookup the base type and then convert to array 89 | let arrayDimensions: number | undefined; 90 | 91 | if (name.endsWith("[]")) { 92 | arrayDimensions = 0; 93 | while(name.endsWith("[]")) { 94 | name = name.substring(0, name.length - 2); 95 | arrayDimensions++; 96 | } 97 | } 98 | 99 | // Value descriptor names are globally unique 100 | let valueDescriptor = this._valueDescriptors.find((valueDescriptor) => valueDescriptor.name === name); 101 | if (valueDescriptor && arrayDimensions) { 102 | valueDescriptor = {...valueDescriptor, arrayDimensions: arrayDimensions}; 103 | } 104 | return valueDescriptor; 105 | } 106 | 107 | public static resolveValueDescriptor(valueHolder: ValueHolder | undefined, descriptorOrValueType: ValueDescriptorHolder | ValueDescriptor | string | undefined): ValueDescriptor | undefined { 108 | let valueDescriptor: ValueDescriptor | undefined; 109 | 110 | if (descriptorOrValueType) { 111 | if (typeof(descriptorOrValueType) === "string") { 112 | valueDescriptor = AssetModelUtil.getValueDescriptor(descriptorOrValueType); 113 | } 114 | if ((descriptorOrValueType as ValueDescriptor).jsonType) { 115 | valueDescriptor = descriptorOrValueType as ValueDescriptor; 116 | } else { 117 | // Must be a value descriptor holder or value holder 118 | valueDescriptor = AssetModelUtil.getValueDescriptor((descriptorOrValueType as ValueDescriptorHolder).type); 119 | } 120 | } 121 | 122 | if (!valueDescriptor && valueHolder) { 123 | // Try and determine the value descriptor based on the value type 124 | valueDescriptor = AssetModelUtil.getValueDescriptor(WellknownValueTypes.JSON); 125 | } 126 | 127 | return valueDescriptor; 128 | } 129 | 130 | public static resolveValueTypeFromValue(value: any): string | undefined { 131 | if (value === null || value === undefined) { 132 | return undefined; 133 | } 134 | 135 | if (typeof value === "number") { 136 | return WellknownValueTypes.NUMBER; 137 | } 138 | if (typeof value === "string") { 139 | return WellknownValueTypes.TEXT; 140 | } 141 | if (typeof value === "boolean") { 142 | return WellknownValueTypes.BOOLEAN; 143 | } 144 | if (Array.isArray(value)) { 145 | let dimensions = 1; 146 | let v = (value as any[]).find(v => v !== undefined && v !== null); 147 | 148 | while (Array.isArray(v)) { 149 | v = (v as any[]).find(v => v !== undefined && v !== null); 150 | dimensions++; 151 | } 152 | 153 | let valueType = this.resolveValueTypeFromValue(v); 154 | 155 | if (!valueType) { 156 | return; 157 | } 158 | 159 | while (dimensions > 0) { 160 | valueType += "[]"; 161 | dimensions--; 162 | } 163 | 164 | return valueType; 165 | } 166 | if (value instanceof Date) { 167 | return WellknownValueTypes.DATEANDTIME; 168 | } 169 | } 170 | 171 | public static getAttributeAndValueDescriptors(assetType: string | undefined, attributeNameOrDescriptor: string | AttributeDescriptor | undefined, attribute?: Attribute): [AttributeDescriptor | undefined, ValueDescriptor | undefined] { 172 | let attributeDescriptor: AttributeDescriptor | undefined; 173 | let valueDescriptor: ValueDescriptor | undefined; 174 | 175 | if (attributeNameOrDescriptor && typeof attributeNameOrDescriptor !== "string") { 176 | attributeDescriptor = attributeNameOrDescriptor as AttributeDescriptor; 177 | } else { 178 | const assetTypeInfo = this.getAssetTypeInfo(assetType || WellknownAssets.THINGASSET); 179 | 180 | if (!assetTypeInfo) { 181 | return [undefined, undefined]; 182 | } 183 | 184 | if (typeof (attributeNameOrDescriptor) === "string") { 185 | attributeDescriptor = this.getAttributeDescriptor(attributeNameOrDescriptor as string, assetTypeInfo); 186 | } 187 | 188 | if (!attributeDescriptor && attribute) { 189 | attributeDescriptor = { 190 | type: attribute.type, 191 | name: attribute.name, 192 | meta: attribute.meta 193 | }; 194 | } 195 | } 196 | 197 | if (attributeDescriptor) { 198 | valueDescriptor = this.getValueDescriptor(attributeDescriptor.type); 199 | } 200 | 201 | return [attributeDescriptor, valueDescriptor]; 202 | } 203 | 204 | public static getMetaItemDescriptor(name?: string): MetaItemDescriptor | undefined { 205 | if (!name) { 206 | return; 207 | } 208 | 209 | // Meta item descriptor names are globally unique 210 | return this._metaItemDescriptors.find((metaItemDescriptor) => metaItemDescriptor.name === name); 211 | } 212 | 213 | public static getAssetDescriptorColour(typeOrDescriptor: string | AssetTypeInfo | AssetDescriptor | undefined, fallbackColor?: string): string | undefined { 214 | const assetDescriptor = this.getAssetDescriptor(typeOrDescriptor); 215 | return assetDescriptor && assetDescriptor.colour ? assetDescriptor.colour : fallbackColor; 216 | } 217 | 218 | public static getAssetDescriptorIcon(typeOrDescriptor: string | AssetTypeInfo | AssetDescriptor | undefined, fallbackIcon?: string): string | undefined { 219 | const assetDescriptor = this.getAssetDescriptor(typeOrDescriptor); 220 | return assetDescriptor && assetDescriptor.icon ? assetDescriptor.icon : fallbackIcon; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /project.gradle: -------------------------------------------------------------------------------- 1 | // Common configuration applied to all projects 2 | import java.nio.file.* 3 | import java.nio.file.attribute.* 4 | import java.util.stream.Collectors 5 | import java.util.stream.Stream 6 | import java.util.stream.StreamSupport 7 | 8 | import static org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS 9 | import static org.apache.tools.ant.taskdefs.condition.Os.isFamily 10 | import static org.jetbrains.gradle.ext.ShortenCommandLine.MANIFEST 11 | import org.jetbrains.gradle.ext.Application 12 | import org.jetbrains.gradle.ext.JUnit 13 | 14 | configurations.all { 15 | resolutionStrategy { 16 | 17 | // check for changing (i.e. SNAPSHOT) updates every build 18 | cacheChangingModulesFor 0, 'seconds' 19 | 20 | //failOnVersionConflict() 21 | 22 | // This has been replaced with eclipse angus implementation 23 | exclude group: "com.sun.activation", module: "jakarta.activation" 24 | 25 | // This has been replaced with org.bouncycastle bcprov-jdk18on 26 | exclude group: "org.bouncycastle", module: "bcprov-jdk15on" 27 | 28 | eachDependency { DependencyResolveDetails details -> 29 | if (details.requested.group == 'org.eclipse.angus' && details.requested.name == 'angus-activation' && details.requested.version == '1.0.0') { 30 | details.useVersion '2.0.0' 31 | } 32 | if (details.requested.group == 'commons-io' && details.requested.name == 'commons-io') { 33 | details.useVersion '2.14.0' 34 | } 35 | } 36 | } 37 | } 38 | 39 | // Ensure git hook creation task is executed 40 | if (project == rootProject) { 41 | 42 | project.afterEvaluate { 43 | 44 | if (rootProject.hasProperty("gradleFileEncrypt")) { 45 | println("File encryption plugin config found, configuring git pre commit hook and decrypt task dependency") 46 | try { 47 | // Write git hook for encryption plugin checks before any commit 48 | def path = Paths.get(rootProject.projectDir.path, ".git/hooks/pre-commit") 49 | def f = path.toFile() 50 | f.text = """#!/bin/sh 51 | echo "***** Running gradle encryption plugin checkFilesGitIgnored task ******" 52 | ./gradlew checkFilesGitIgnoredNew 53 | status=\$? 54 | if [ \$status != 0 ]; then 55 | echo "***** One or more encrypted files are not listed in a .gitignore - please add to prevent unencrypted version of file(s) from being committed *****" 56 | fi 57 | exit \$status 58 | """ 59 | Set perms = Files.readAttributes(path, PosixFileAttributes.class).permissions() 60 | perms.add(PosixFilePermission.OWNER_WRITE) 61 | perms.add(PosixFilePermission.OWNER_READ) 62 | perms.add(PosixFilePermission.OWNER_EXECUTE) 63 | perms.add(PosixFilePermission.GROUP_WRITE) 64 | perms.add(PosixFilePermission.GROUP_READ) 65 | perms.add(PosixFilePermission.GROUP_EXECUTE) 66 | perms.add(PosixFilePermission.OTHERS_READ) 67 | perms.add(PosixFilePermission.OTHERS_EXECUTE) 68 | Files.setPosixFilePermissions(path, perms) 69 | } catch (Exception ignored) {} 70 | 71 | // Add dependency on decrypt task for deployment installDist only if GFE_PASSWORD defined 72 | def password = System.env.GFE_PASSWORD 73 | if (password != null) { 74 | Task decryptTask = getTasksByName("decryptFiles", false)[0] 75 | 76 | try { 77 | def installDist = tasks.getByPath(":deployment:installDist") 78 | installDist.dependsOn decryptTask 79 | installDist.mustRunAfter(decryptTask) 80 | } catch (Exception ex) { 81 | println("Failed to add decryptFiles task dependency: " + ex) 82 | } 83 | } 84 | } else { 85 | // Remove git hook 86 | try { 87 | Files.delete(Paths.get(rootProject.projectDir.path, ".git/hooks/pre-commit")) 88 | } catch (Exception ignored) { 89 | } 90 | } 91 | } 92 | } 93 | 94 | 95 | // Configure Conditional plugins 96 | if (project == rootProject) { 97 | apply plugin: "org.jetbrains.gradle.plugin.idea-ext" 98 | 99 | // Configure IDEA 100 | if (project.hasProperty("idea") && idea.project) { 101 | // IDEA settings 102 | idea.project.settings { 103 | compiler { 104 | javac { 105 | javacAdditionalOptions = "-parameters" 106 | } 107 | } 108 | runConfigurations { 109 | defaults(JUnit) { 110 | shortenCommandLine = MANIFEST 111 | workingDirectory = projectDir.toString() 112 | } 113 | defaults(Application) { 114 | mainClass = 'org.openremote.manager.Main' 115 | shortenCommandLine = MANIFEST 116 | workingDirectory = projectDir.toString() 117 | } 118 | "Empty"(Application) { 119 | moduleName = getProject().idea.module.name + ".openremote.manager.main" 120 | } 121 | "Custom Deployment"(Application) { 122 | moduleName = "${getProject().idea.module.name}.setup.main" 123 | envs = [ 124 | OR_MAP_SETTINGS_PATH: "deployment/map/mapsettings.json", 125 | OR_MAP_TILES_PATH: "deployment/map/mapdata.mbtiles", 126 | OR_CUSTOM_APP_DOCROOT: "deployment/manager/app", 127 | OR_CONSOLE_APP_CONFIG_DOCROOT: "deployment/manager/consoleappconfig" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | // Give test projects more memory (Gradle 5 reduced this to 512MB) 136 | subprojects { 137 | tasks.withType(Test) { 138 | maxHeapSize = "2g" 139 | } 140 | } 141 | 142 | // Default repositories for dependency resolution 143 | repositories { 144 | mavenLocal() 145 | // Needs to be above central due to org.geotools:gt-main issues 146 | maven { 147 | url = "https://repo.osgeo.org/repository/release/" 148 | } 149 | mavenCentral() 150 | maven { 151 | url = "https://pkgs.dev.azure.com/OpenRemote/OpenRemote/_packaging/OpenRemote/maven/v1" 152 | } 153 | maven { 154 | url = "https://central.sonatype.com/repository/maven-snapshots" 155 | } 156 | maven { 157 | url = "https://repo.jenkins-ci.org/releases/" 158 | } 159 | maven { 160 | url = 'https://jitpack.io' 161 | } 162 | } 163 | 164 | // Eclipse needs help 165 | apply plugin: "eclipse" 166 | 167 | // Intellij needs help 168 | apply plugin: 'idea' 169 | // Use the same output directories in IDE as in gradle 170 | idea { 171 | module { 172 | outputDir = file('build/classes/main') 173 | testOutputDir = file('build/classes/test') 174 | excludeDirs += file(".node") 175 | excludeDirs += file("node_modules") 176 | excludeDirs += file("dist") 177 | excludeDirs += file("lib") 178 | excludeDirs += file("build") 179 | } 180 | } 181 | 182 | /** 183 | * This defines reusable config for the typescript generator plugin 184 | */ 185 | def createTSGeneratorConfigForModel(String outputFileName, Project...customProjectsToScan) { 186 | def config = createTSGeneratorConfig(false, outputFileName, customProjectsToScan) << 187 | { 188 | extensions = [ 189 | "org.openremote.model.util.AssetModelInfoExtension", 190 | "CustomExtension", 191 | "JsonSerializeExtension" 192 | ] 193 | customTypeMappings = [ 194 | "com.fasterxml.jackson.databind.node.ObjectNode:{ [id: string]: any }", 195 | "java.lang.Class:string", 196 | "org.openremote.model.attribute.MetaItem:any" 197 | ] 198 | customTypeProcessor = "CustomTypeProcessor" 199 | generateInfoJson = true 200 | } 201 | return config 202 | } 203 | def createTSGeneratorConfigForClient(String outputFileName, File modelInfoJson, Project...customProjectsToScan) { 204 | def config = createTSGeneratorConfig(true, outputFileName, customProjectsToScan) << 205 | { 206 | extensions = [ 207 | "CustomExtension", 208 | "JsonSerializeExtension", 209 | "CustomAggregatedApiClient", 210 | "cz.habarta.typescript.generator.ext.AxiosClientExtension" 211 | ] 212 | customTypeMappings = [ 213 | "com.fasterxml.jackson.databind.node.ObjectNode:{ [id: string]: any }", 214 | "java.lang.Class:string", 215 | "org.openremote.model.attribute.MetaItem:any", 216 | "org.openremote.model.asset.Asset:Model.Asset", 217 | "org.openremote.model.asset.AssetDescriptor:Model.AssetDescriptor", 218 | "org.openremote.model.asset.agent.Agent:Model.Agent", 219 | "org.openremote.model.asset.agent.AgentDescriptor:Model.AgentDescriptor", 220 | "org.openremote.model.value.MetaItemDescriptor:Model.MetaItemDescriptor", 221 | "org.openremote.model.value.ValueDescriptor:Model.ValueDescriptor" 222 | ] 223 | moduleDependencies = [ 224 | cz.habarta.typescript.generator.ModuleDependency.module( 225 | "model", 226 | "Model", 227 | modelInfoJson, 228 | (String) null, 229 | (String) null 230 | ) 231 | ] 232 | restNamespacing = "perResource" 233 | } 234 | return config 235 | } 236 | def createTSGeneratorConfig(boolean outputAPIClient, String outputFileName, Project...customProjectsToScan) { 237 | 238 | def classPatternGlobs = Arrays.stream(customProjectsToScan).flatMap { project -> 239 | return project.sourceSets.findByName('main').java.srcDirs.stream().map { 240 | def srcPath = it 241 | def isPackageDir = true 242 | while (srcPath != null && isPackageDir) { 243 | def files = srcPath.listFiles() 244 | isPackageDir = files != null && files.length == 1 && files[0].isDirectory() 245 | if (isPackageDir) { 246 | srcPath = files[0] 247 | } 248 | } 249 | Path packagePath = it.toPath().relativize(srcPath.toPath()) 250 | return StreamSupport 251 | .stream(packagePath.spliterator(), false) 252 | .map(Path::toString) 253 | .collect(Collectors.joining(".")) + (outputAPIClient ? ".**Resource" : ".**") 254 | } 255 | }.toList() 256 | 257 | def baseClassPattern = outputAPIClient 258 | ? customProjectsToScan.length == 0 ? [ "org.openremote.model.**Resource" ] : [] 259 | : [ "org.openremote.model.**" ] 260 | 261 | return { 262 | jsonLibrary = "jackson2" 263 | classPatterns = baseClassPattern + classPatternGlobs 264 | customTypeNamingFunction = "function(name, simpleName) { if (name.indexOf(\"\$\") > 0) return name.substr(name.lastIndexOf(\".\")+1).replace(\"\$\",\"\"); }" 265 | excludeClassPatterns = [ 266 | "org.openremote.model.event.shared.*Filter**", 267 | "org.openremote.model.util.**", 268 | "org.openremote.model.flow.**", 269 | "java.io.**", 270 | "java.lang.**", 271 | "org.hibernate.**", 272 | "jakarta.**" 273 | ] 274 | mapEnum = cz.habarta.typescript.generator.EnumMapping.asEnum 275 | mapDate = cz.habarta.typescript.generator.DateMapping.asNumber 276 | optionalProperties = "all" // TODO: cleanup model to be more explicit about optional params 277 | outputFileType = "implementationFile" 278 | outputKind = "module" 279 | outputFile = outputFileName 280 | jackson2Configuration = [ 281 | fieldVisibility: "ANY", 282 | creatorVisibility: "ANY", 283 | getterVisibility: "NONE", 284 | isGetterVisibility: "NONE", 285 | setterVisibility: "NONE" 286 | ] 287 | jackson2Modules = [ 288 | "com.fasterxml.jackson.datatype.jdk8.Jdk8Module", 289 | "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", 290 | "com.fasterxml.jackson.module.paramnames.ParameterNamesModule" 291 | ] 292 | } 293 | } 294 | 295 | def resolveTask(String path) { 296 | tasks.getByPath(path) 297 | } 298 | 299 | def getYarnInstallTask() { 300 | def customPackageJsonFile = Paths.get(rootProject.projectDir.path, "package.json").toFile() 301 | if (!customPackageJsonFile.exists()) { 302 | // No custom project yarn package.json so use standard openremote repo package.json 303 | return resolveTask(":yarnInstall") 304 | } else { 305 | return tasks.getByPath(":yarnInstall") 306 | } 307 | } 308 | 309 | 310 | ext { 311 | resolveTask = this.&resolveTask 312 | getYarnInstallTask = this.&getYarnInstallTask 313 | createTSGeneratorConfigForClient = this.&createTSGeneratorConfigForClient 314 | createTSGeneratorConfigForModel = this.&createTSGeneratorConfigForModel 315 | } 316 | 317 | // Add UI tasks 318 | ext.npmCommand = { 319 | cmd -> 320 | isFamily(FAMILY_WINDOWS) ? "${cmd}.cmd" : cmd 321 | } 322 | 323 | // Add yarn tasks 324 | tasks.register('yarnInstall', Exec) { 325 | commandLine npmCommand("yarn"), "install" 326 | } 327 | tasks.register('yarnInstallForce', Exec) { 328 | commandLine npmCommand("yarn"), "install", "--force" 329 | } 330 | 331 | tasks.register('npmClean', Exec) { 332 | dependsOn getYarnInstallTask() 333 | commandLine npmCommand("yarn"), "run", "clean" 334 | } 335 | tasks.register('npmBuild', Exec) { 336 | mustRunAfter npmClean 337 | dependsOn getYarnInstallTask() 338 | commandLine npmCommand("yarn"), "run", "build" 339 | } 340 | tasks.register('npmServe', Exec) { 341 | dependsOn getYarnInstallTask() 342 | commandLine npmCommand("yarn"), "run", "serve" 343 | } 344 | tasks.register('npmPrepare', Exec) { 345 | dependsOn getYarnInstallTask() 346 | commandLine npmCommand("yarn"), "run", "prepublishOnly" 347 | } 348 | tasks.register('npmPublish', Exec) { 349 | dependsOn getYarnInstallTask() 350 | commandLine npmCommand("yarn"), "publish" 351 | } 352 | tasks.register('npmServeProduction', Exec) { 353 | dependsOn getYarnInstallTask() 354 | commandLine npmCommand("yarn"), "run", "serveProduction" 355 | } 356 | 357 | // Add typescript tasks 358 | tasks.register('tscWatch', Exec) { 359 | commandLine npmCommand("npx"), "tsc", "-b", "--watch" 360 | } 361 | 362 | // Configure Java build 363 | plugins.withType(JavaPlugin).whenPluginAdded { 364 | 365 | // Use Java 21 366 | tasks.withType(JavaCompile) { 367 | sourceCompatibility = JavaVersion.VERSION_21 368 | targetCompatibility = JavaVersion.VERSION_21 369 | def warnLogFile = file("$buildDir/${name}Warnings.log") 370 | logging.addStandardErrorListener(new StandardOutputListener() { 371 | void onOutput(CharSequence output) { 372 | warnLogFile << output 373 | } 374 | }) 375 | options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation", "-parameters"] 376 | options.encoding = 'UTF-8' 377 | } 378 | 379 | // Allow dependencyInsight checks across all projects 380 | tasks.register('allDependencyInsight', DependencyInsightReportTask) {} 381 | 382 | base { 383 | // JAR/ZIP base name is the fully qualified subproject name 384 | archivesName = "${rootProject.name}${path.replaceAll(":", "-")}" 385 | } 386 | } 387 | 388 | // POM generator 389 | --------------------------------------------------------------------------------