diff --git a/IoTDBJDBC/SetupAndDataAccessNotes/GrafanaPlugin.txt b/IoTDBJDBC/SetupAndDataAccessNotes/GrafanaPlugin.txt
new file mode 100644
index 0000000..47dc1c1
--- /dev/null
+++ b/IoTDBJDBC/SetupAndDataAccessNotes/GrafanaPlugin.txt
@@ -0,0 +1,2 @@
+The development team are building a grafana plugin that can be used to connect to the time series DB
+this is available at https://github.com/oracle-samples/oracle-telemetry-streaming
\ No newline at end of file
diff --git a/IoTDBJDBC/SetupNotes/PromQL.txt b/IoTDBJDBC/SetupAndDataAccessNotes/PromQL.txt
similarity index 100%
rename from IoTDBJDBC/SetupNotes/PromQL.txt
rename to IoTDBJDBC/SetupAndDataAccessNotes/PromQL.txt
diff --git a/IoTDBJDBC/SetupAndDataAccessNotes/SQLAccess.md b/IoTDBJDBC/SetupAndDataAccessNotes/SQLAccess.md
new file mode 100644
index 0000000..93dc0a4
--- /dev/null
+++ b/IoTDBJDBC/SetupAndDataAccessNotes/SQLAccess.md
@@ -0,0 +1,19 @@
+#SQL Access
+## Why ?
+Though the primary access will be through promQL that is more for monitoring access. For broader access, for example combining business and IoT data (assuming both are in the same DB) there is also the option to access the data using "normal" SQL which allow access to the individual (not time series) data
+##How
+You need to connect to the database using your "normal" DB connect mechanisms (JDBC, SQLDeveloper, SQLPlus etc.) then access the TELEMETRY_METRICS data
+
+'select * from TELEMETRY_METRICS order by metric_time_epoch desc fetch first 10 rows only'
+
+|METRIC_NAME|METRIC_TAGS|METRIC_VALUE|METRIC_EPOCH_TIMESTAMP|
+|:-----:|:-----:|:-----:|:-----:|
+|iot.normalized.inverter_power_watts_point_in_time|{"iot_content_path":"InverterPowerWattsPointInTime","iot_content_type":"DECIMAL","iot_digital_twin_instance_id":"ocid1.iotdigitaltwininstance.oc1.uk-london-1.amaaaaaauevftmr7h6t75eifevhfihvbwazwi43ndgrauaawue22d6svdpna","iot_digital_twin_model_id":"ocid1.iotdigitaltwinmodel.oc1.uk-london-1.amaaaaaauevftmr7wuslrtqim6zflcxhqaprmzib74p76ev64g4zqy7edbva","iot_digital_twin_model_name":"homebattery","service_name":"IoTDBJDBC"}|-1947|1783075316.509|
+|iot.normalized.discharge_power |{"iot_content_path":"DischargePower","iot_content_type":"DECIMAL","iot_digital_twin_instance_id":"ocid1.iotdigitaltwininstance.oc1.uk-london-1.amaaaaaauevftmr7h6t75eifevhfihvbwazwi43ndgrauaawue22d6svdpna","iot_digital_twin_model_id":"ocid1.iotdigitaltwinmodel.oc1.uk-london-1.amaaaaaauevftmr7wuslrtqim6zflcxhqaprmzib74p76ev64g4zqy7edbva","iot_digital_twin_model_name":"homebattery","service_name":"IoTDBJDBC"}| 0 |1783075316.509|
+|iot.normalized.capacity_remaining |{"iot_content_path":"CapacityRemaining","iot_content_type":"DECIMAL","iot_digital_twin_instance_id":"ocid1.iotdigitaltwininstance.oc1.uk-london-1.amaaaaaauevftmr7h6t75eifevhfihvbwazwi43ndgrauaawue22d6svdpna","iot_digital_twin_model_id":"ocid1.iotdigitaltwinmodel.oc1.uk-london-1.amaaaaaauevftmr7wuslrtqim6zflcxhqaprmzib74p76ev64g4zqy7edbva","iot_digital_twin_model_name":"homebattery","service_name":"IoTDBJDBC"}| 8406 |1783075316.509|
+|iot.normalized.reserved_charge_percentage |{"iot_content_path":"ReservedChargePercentage","iot_content_type":"DECIMAL","iot_digital_twin_instance_id":"ocid1.iotdigitaltwininstance.oc1.uk-london-1.amaaaaaauevftmr7h6t75eifevhfihvbwazwi43ndgrauaawue22d6svdpna","iot_digital_twin_model_id":"ocid1.iotdigitaltwinmodel.oc1.uk-london-1.amaaaaaauevftmr7wuslrtqim6zflcxhqaprmzib74p76ev64g4zqy7edbva","iot_digital_twin_model_name":"homebattery","service_name":"IoTDBJDBC"} |5 |1783075316.509|
+
+
+Important to note. Open Telemetry recommends the use of dot separated namespaces with the names in snake_case, for example 'iot.normalized.inverter_power_watts_point_in_time' the IoTDBJDBC upload code supports this, currently however there is a "feature" in the time series DB code that on ingest converts the names of tags (also known as attributes) into only snake_case (e.g. iot.digital_twin.instance_id, to iot_digital_twin_instance_id) This was implemented to support PromQL (which seems to only like the use of _) but it means that when querying the data the tags may not be what you expect. This is being fixed, but for now when using the tags (which you will need to use to identify the specific devices) remember it's iot_digital_twin_instance_id
+
+Also note that the upload code tries to do C like boolean mappings, so true is 1 and false is 0;
\ No newline at end of file
diff --git a/IoTDBJDBC/SetupNotes/TimeSeriesDatabase.txt b/IoTDBJDBC/SetupAndDataAccessNotes/TimeSeriesDatabase.txt
similarity index 100%
rename from IoTDBJDBC/SetupNotes/TimeSeriesDatabase.txt
rename to IoTDBJDBC/SetupAndDataAccessNotes/TimeSeriesDatabase.txt
diff --git a/IoTDBJDBC/SetupNotes/TimeSeriesMessageHandlersSetup.txt b/IoTDBJDBC/SetupAndDataAccessNotes/TimeSeriesMessageHandlersSetup.txt
similarity index 100%
rename from IoTDBJDBC/SetupNotes/TimeSeriesMessageHandlersSetup.txt
rename to IoTDBJDBC/SetupAndDataAccessNotes/TimeSeriesMessageHandlersSetup.txt
diff --git a/IoTDBJDBC/pom.xml b/IoTDBJDBC/pom.xml
index b8a139f..002ced6 100644
--- a/IoTDBJDBC/pom.xml
+++ b/IoTDBJDBC/pom.xml
@@ -53,7 +53,7 @@ SOFTWARE. -->
jar
21
21
- 3.74.2
+ 3.91.0
1.18.46
2.0.7
diff --git a/IoTDemoProxyGateway/.classpath b/IoTDemoProxyGateway/.classpath
new file mode 100644
index 0000000..96c6daa
--- /dev/null
+++ b/IoTDemoProxyGateway/.classpath
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IoTDemoProxyGateway/.factorypath b/IoTDemoProxyGateway/.factorypath
new file mode 100644
index 0000000..eec09fc
--- /dev/null
+++ b/IoTDemoProxyGateway/.factorypath
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IoTDemoProxyGateway/.gitignore b/IoTDemoProxyGateway/.gitignore
new file mode 100644
index 0000000..b436ecb
--- /dev/null
+++ b/IoTDemoProxyGateway/.gitignore
@@ -0,0 +1,4 @@
+/config/
+/configsecure/
+/target/
+/samples/
diff --git a/IoTDemoProxyGateway/.project b/IoTDemoProxyGateway/.project
new file mode 100644
index 0000000..852cd73
--- /dev/null
+++ b/IoTDemoProxyGateway/.project
@@ -0,0 +1,23 @@
+
+
+ IoTDemoProxyGateway
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/IoTDemoProxyGateway/.settings/org.eclipse.core.resources.prefs b/IoTDemoProxyGateway/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..29abf99
--- /dev/null
+++ b/IoTDemoProxyGateway/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/=UTF-8
diff --git a/IoTDemoProxyGateway/.settings/org.eclipse.jdt.apt.core.prefs b/IoTDemoProxyGateway/.settings/org.eclipse.jdt.apt.core.prefs
new file mode 100644
index 0000000..eadaf34
--- /dev/null
+++ b/IoTDemoProxyGateway/.settings/org.eclipse.jdt.apt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=true
+org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations
+org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations
+org.eclipse.jdt.apt.processorOptions/micronaut.processing.group=com.oracle.demo.timg.iot.iotproxygateway
+org.eclipse.jdt.apt.processorOptions/micronaut.processing.module=IoTDemoProxyGateway
diff --git a/IoTDemoProxyGateway/.settings/org.eclipse.jdt.core.prefs b/IoTDemoProxyGateway/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..943cc39
--- /dev/null
+++ b/IoTDemoProxyGateway/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,10 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
+org.eclipse.jdt.core.compiler.compliance=21
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=21
diff --git a/IoTDemoProxyGateway/.settings/org.eclipse.m2e.core.prefs b/IoTDemoProxyGateway/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/IoTDemoProxyGateway/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/IoTDemoProxyGateway/pom.xml b/IoTDemoProxyGateway/pom.xml
new file mode 100644
index 0000000..d84d010
--- /dev/null
+++ b/IoTDemoProxyGateway/pom.xml
@@ -0,0 +1,217 @@
+
+
+ 4.0.0
+ com.oracle.demo.timg.iot.iotproxygateway
+ IoTDemoProxyGateway
+ 0.1
+ ${packaging}
+
+
+ io.micronaut.platform
+ micronaut-parent
+ 4.10.16
+
+
+
+ jar
+ 21
+ 21
+ 4.10.16
+ netty
+ true
+ false
+ com.oracle.demo.timg.iot.iotproxygateway.aot.generated
+ com.oracle.demo.timg.iot.iotproxygateway.Application
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+
+
+
+
+ org.apache.logging.log4j
+ log4j-bom
+ 2.25.3
+ pom
+ import
+
+
+
+
+
+
+
+
+ io.micronaut
+ micronaut-http-client
+ compile
+
+
+
+
+
+ io.micronaut.mqtt
+ micronaut-mqtt-hivemq
+ compile
+
+
+
+ io.micronaut.serde
+ micronaut-serde-jackson
+ compile
+
+
+
+ io.micronaut.validation
+ micronaut-validation
+ compile
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ compile
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ compile
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ runtime
+
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ runtime
+
+
+
+ org.yaml
+ snakeyaml
+ runtime
+
+
+
+ io.micronaut.testresources
+ micronaut-test-resources-client
+ provided
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+ io.micronaut.test
+ micronaut-test-junit5
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+
+ io.micronaut.maven
+ micronaut-maven-plugin
+
+ true
+ aot-${packaging}.properties
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ io.micronaut
+ micronaut-inject-java
+ ${micronaut.core.version}
+
+
+ io.micronaut
+ micronaut-graal
+ ${micronaut.core.version}
+
+
+ io.micronaut
+ micronaut-http-validation
+ ${micronaut.core.version}
+
+
+ io.micronaut.serde
+ micronaut-serde-processor
+ ${micronaut.serialization.version}
+
+
+ io.micronaut
+ micronaut-inject
+
+
+
+
+ io.micronaut.validation
+ micronaut-validation-processor
+ ${micronaut.validation.version}
+
+
+ io.micronaut
+ micronaut-inject
+
+
+
+
+
+ -Amicronaut.processing.group=com.oracle.demo.timg.iot.iotproxygateway
+ -Amicronaut.processing.module=IoTDemoProxyGateway
+
+
+
+
+
+
+
+
diff --git a/IoTDemoProxyGateway/sampleconfig/iotservice.yml b/IoTDemoProxyGateway/sampleconfig/iotservice.yml
new file mode 100644
index 0000000..ba4c44e
--- /dev/null
+++ b/IoTDemoProxyGateway/sampleconfig/iotservice.yml
@@ -0,0 +1,275 @@
+gateway:
+ identity:
+ name: "HomeAssistantGateway1"
+ devicekey: "hagateway"
+ endpoint:
+ base: "house/homeassistant"
+ gatewaystatssubpath: "gateway/stats"
+ gatewayconfigsubpath: "gateway/config"
+ entitiessubpath: "entities"
+ config:
+ enabled: true
+ publishrate: "120s"
+ initialdelay: "10s"
+ stats:
+ enabled: true
+ publishrate: "30s"
+ initialdelay: "5s"
+
+home-assistant:
+ api:
+ port: 8123
+ host:
+ protocol: "http"
+ url: ${home-assistant.api.protocol}://${home-assistant.api.host}:${home-assistant.api.port}
+ monitored-entities:
+ - name: "HomeBatteryManualReserve"
+ doupload: true
+ initaldelay: "PT3s"
+ retrievalrate: "PT60s"
+ devicekey: "SnnManRsrvSttngs"
+ endpoint: "homebatterymanualreservesettings"
+ monitoredentities:
+ - name: "Repeat manual reserve daily"
+ entityid: "input_boolean.sonnenmanualreservewindowrepeatdaily"
+ iottype: "SWITCH"
+ fieldname: "repeatmanualreserveenabled"
+ sendmode: "ON_CHANGE"
+ - name: "Manual reserve enabled"
+ entityid: "input_boolean.sonnenmanualreservewindowenabled"
+ iottype: "SWITCH"
+ fieldname: "manualreserveenabled"
+ sendmode: "ON_CHANGE"
+ - name: "Sonnen charge level"
+ doupload: true
+ initaldelay: "PT2s"
+ retrievalrate: "PT30s"
+ devicekey: "SnnBatUsrChrg"
+ endpoint: "energy"
+ monitoredentities:
+ - name: "Sonnen user charge level"
+ entityid: "sensor.sonnenbatterie_200357_state_charge_user"
+ iottype: "PERCENT"
+ fieldname: "userchargepercent"
+ sendmode: "ON_REPORT"
+ - name: "UtilityLuminance"
+ doupload: true
+ initaldelay: "PT5s"
+ retrievalrate: "PT10s"
+ devicekey: "UtilIlluminance"
+ endpoint: "illuminance"
+ monitoredentities:
+ - name: "Utility Illuminance"
+ entityid: "sensor.utility_room_myggspray_utility_illuminance"
+ fieldname: "lux"
+ sendmode: "ON_CHANGE"
+ iottype: "LUMINANCE"
+ - name: "Kitchen Door"
+ doupload: true
+ initaldelay: "PT4s"
+ retrievalrate: "PT10s"
+ devicekey: "MyggbettKtchnDr"
+ endpoint: "door"
+ monitoredentities:
+ - name: "Kitchen door sensor"
+ entityid: "binary_sensor.myggbett_kitchen_door"
+ fieldname: "dooropen"
+ sendmode: "ON_CHANGE"
+ iottype: "BINARY_ON_OFF"
+ - name: "Garden Door"
+ doupload: true
+ initaldelay: "PT5s"
+ retrievalrate: "PT10s"
+ devicekey: "MyggbettGrdnDr"
+ endpoint: "door"
+ monitoredentities:
+ - name: "Garden door sensor"
+ entityid: "binary_sensor.myggbett_conservatory_door"
+ fieldname: "dooropen"
+ sendmode: "ON_CHANGE"
+ iottype: "BINARY_ON_OFF"
+ - name: "Front Door"
+ doupload: true
+ initaldelay: "PT5s"
+ retrievalrate: "PT10s"
+ devicekey: "MyggbettFrntDr"
+ endpoint: "door"
+ monitoredentities:
+ - name: "Front door sensor"
+ entityid: "binary_sensor.myggbett_front_door"
+ fieldname: "dooropen"
+ sendmode: "ON_CHANGE"
+ iottype: "BINARY_ON_OFF"
+ - name: "Garage Door"
+ doupload: true
+ initaldelay: "PT5s"
+ retrievalrate: "PT10s"
+ devicekey: "MyggbettGrgDr"
+ endpoint: "door"
+ monitoredentities:
+ - name: "Garage door sensor"
+ entityid: "binary_sensor.myggbett_garage_door"
+ fieldname: "dooropen"
+ sendmode: "ON_CHANGE"
+ iottype: "BINARY_ON_OFF"
+ - name: "Filtered Grid Consumption"
+ doupload: true
+ initaldelay: "PT7s"
+ retrievalrate: "PT10s"
+ devicekey: "FiltGridCons"
+ endpoint: "power"
+ monitoredentities:
+ - name: "Filtered grid"
+ entityid: "sensor.filtered_grid_consumption"
+ fieldname: "watts"
+ sendmode: "ALWAYS"
+ iottype: "POWER_WATTS"
+ - name: "Solar Generation"
+ doupload: true
+ initaldelay: "PT8s"
+ retrievalrate: "PT10s"
+ devicekey: "SolGenTotal"
+ endpoint: "power"
+ monitoredentities:
+ - name: "Solar Generation Combined"
+ entityid: "input_number.solargenerationcombinedcurrent"
+ fieldname: "watts"
+ sendmode: "ALWAYS"
+ iottype: "POWER_WATTS"
+ - name: "Car Charger Power"
+ doupload: true
+ initaldelay: "PT8s"
+ retrievalrate: "PT10s"
+ devicekey: "CrChrgrPwr"
+ endpoint: "power"
+ monitoredentities:
+ - name: "Car charging rate"
+ entityid: "sensor.myenergi_zappi_20758989_power_ct_internal_load"
+ fieldname: "watts"
+ sendmode: "ALWAYS"
+ iottype: "POWER_WATTS"
+ - name: "Indoor Temperature"
+ doupload: true
+ initaldelay: "PT3s"
+ retrievalrate: "PT60s"
+ devicekey: "AvgIndrTmp"
+ endpoint: "temperature"
+ monitoredentities:
+ - name: "Average Indoor Temperature"
+ entityid: "sensor.avgindoortemperature"
+ fieldname: "temperature"
+ sendmode: "ALWAYS"
+ iottype: "TEMPERATURE"
+ - name: "Outdoor Temperature"
+ doupload: true
+ initaldelay: "PT3s"
+ retrievalrate: "PT60s"
+ devicekey: "AvgOtdrTmp"
+ endpoint: "temperature"
+ monitoredentities:
+ - name: "Average Outdoor Temperature"
+ entityid: "sensor.avgoutdoortemperature"
+ fieldname: "temperature"
+ sendmode: "ALWAYS"
+ iottype: "TEMPERATURE"
+ - name: "Indoor Relative Humidity"
+ doupload: true
+ initaldelay: "PT2s"
+ retrievalrate: "PT60s"
+ devicekey: "AvgIndrRltvHmdty"
+ endpoint: "relativehumidity"
+ monitoredentities:
+ - name: "Average Indoor Relative Humidity"
+ entityid: "sensor.avgindoorhumidity"
+ fieldname: "relativehumidity"
+ sendmode: "ALWAYS"
+ iottype: "RELATIVE_HUMIDITY"
+ - name: "Outdoor Relative Humidity"
+ doupload: true
+ initaldelay: "PT2s"
+ retrievalrate: "PT60s"
+ devicekey: "AvgOtdrRltvHmdty"
+ endpoint: "relativehumidity"
+ monitoredentities:
+ - name: "Average Outdoor Relative Humidity"
+ entityid: "sensor.avgoutdoorhumidity"
+ fieldname: "relativehumidity"
+ sendmode: "ALWAYS"
+ iottype: "RELATIVE_HUMIDITY"
+ - name: "Air Pressure"
+ doupload: true
+ initaldelay: "PT6s"
+ retrievalrate: "PT60s"
+ devicekey: "AvgArPrssr"
+ endpoint: "airpressure"
+ monitoredentities:
+ - name: "Average Air Pressure"
+ entityid: "sensor.avgairpressure"
+ fieldname: "airpressure"
+ sendmode: "ALWAYS"
+ iottype: "AIR_PRESSURE"
+ - name: "Car Charger Energy"
+ doupload: true
+ initaldelay: "PT7s"
+ retrievalrate: "PT60s"
+ devicekey: "CrChrgTdy"
+ endpoint: "kilowatthours"
+ monitoredentities:
+ - name: "Energy to car today"
+ entityid: "sensor.myenergi_zappi_20758989_energy_used_today"
+ fieldname: "kilowatthours"
+ sendmode: "ALWAYS"
+ iottype: "ENERGY_KILO_WATT_HOURS"
+ - name: "Daily groid energy flows"
+ doupload: true
+ initaldelay: "PT1s"
+ retrievalrate: "PT60s"
+ devicekey: "DlyGrdFlws"
+ endpoint: "dailygridenergyflows"
+ monitoredentities:
+ - name: "Imported from grid today"
+ entityid: "sensor.myenergi_hub_20758989_grid_import_today_2"
+ fieldname: "importkwh"
+ sendmode: "ALWAYS"
+ iottype: "ENERGY_KILO_WATT_HOURS"
+ - name: "Exported to grid today"
+ entityid: "sensor.myenergi_hub_20758989_grid_export_today_2"
+ fieldname: "exportkwh"
+ sendmode: "ALWAYS"
+ iottype: "ENERGY_KILO_WATT_HOURS"
+
+mqtt:
+ enabled: true
+ broker:
+ #host: localhost
+ host: ${private.devicehost}
+ # 8883 for iot service, 1883 for local mosquito
+ #port: 1883
+ port: 8883
+ # mqtt protocol - either tcp for local or ssl for remote
+ #protocol: tcp
+ protocol: ssl
+ client:
+ # Unique identifier for this MQTT client
+ # The gateway name, not any device name
+ client-id: ${gateway.identity.name}
+
+ # MQTT broker connection
+ server-uri: ${mqtt.broker.protocol}://${mqtt.broker.host}:${mqtt.broker.port}
+
+ # **GATEWAY** Authentication id (external key as assigned or auto generated at creation)
+ user-name: ${gateway.identity.devicekey}
+ # **GATEWAY** Authentication password
+ password: ${private.mqtt.client.passwordoracle}
+
+ # Optional connection settings
+ clean-session: false
+ automatic-reconnect: true
+ connection-timeout: "PT30s"
+ keep-alive-interval: 60
+
+ will-message:
+ topic: home/homeassistantproxygateway/${gateway.identity.name}
+ payload: Device ${gateway.identity.name} is unavailable
+ qos: 0
+ retained: true
diff --git a/IoTDemoProxyGateway/sampleconfigsecure/configsecure.yml b/IoTDemoProxyGateway/sampleconfigsecure/configsecure.yml
new file mode 100644
index 0000000..f663c49
--- /dev/null
+++ b/IoTDemoProxyGateway/sampleconfigsecure/configsecure.yml
@@ -0,0 +1,3 @@
+homeAssistant:
+ api:
+ auth_token: ""
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/sampleconfigsecure/private-settings.yml b/IoTDemoProxyGateway/sampleconfigsecure/private-settings.yml
new file mode 100644
index 0000000..d07eb5c
--- /dev/null
+++ b/IoTDemoProxyGateway/sampleconfigsecure/private-settings.yml
@@ -0,0 +1,6 @@
+private:
+ devicehost: ""
+ mqtt:
+ client:
+ brokeroracle: ${private.devicehost}
+ passwordoracle: ""
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/samples/hastatesolarnow.json b/IoTDemoProxyGateway/samples/hastatesolarnow.json
new file mode 100644
index 0000000..dd610ed
--- /dev/null
+++ b/IoTDemoProxyGateway/samples/hastatesolarnow.json
@@ -0,0 +1,23 @@
+{
+ "entity_id": "input_number.solargenerationcombinedcurrent",
+ "state": "1030.31",
+ "attributes": {
+ "initial": null,
+ "editable": true,
+ "min": 0.0,
+ "max": 10000000.0,
+ "step": 1.0,
+ "mode": "box",
+ "unit_of_measurement": "kWh",
+ "icon": "mdi:solar-power-variant",
+ "friendly_name": "SolarGenerationCombinedCurrent"
+ },
+ "last_changed": "2026-07-02T10:23:21.838557+00:00",
+ "last_reported": "2026-07-02T10:23:21.838557+00:00",
+ "last_updated": "2026-07-02T10:23:21.838557+00:00",
+ "context": {
+ "id": "01KWH5NH7DHWXMZBNAVHRETKYT",
+ "parent_id": "01KWH5NH79058RDFHAC73D09X5",
+ "user_id": null
+ }
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/samples/hastateutilluminmance.json b/IoTDemoProxyGateway/samples/hastateutilluminmance.json
new file mode 100644
index 0000000..ce2ff52
--- /dev/null
+++ b/IoTDemoProxyGateway/samples/hastateutilluminmance.json
@@ -0,0 +1,18 @@
+{
+ "entity_id": "sensor.utility_room_myggspray_utility_illuminance",
+ "state": "222.0",
+ "attributes": {
+ "state_class": "measurement",
+ "unit_of_measurement": "lx",
+ "device_class": "illuminance",
+ "friendly_name": "MYGGSPRAY utility Illuminance"
+ },
+ "last_changed": "2026-07-02T10:44:40.613085+00:00",
+ "last_reported": "2026-07-02T10:44:40.613085+00:00",
+ "last_updated": "2026-07-02T10:44:40.613085+00:00",
+ "context": {
+ "id": "01KWH6WJ153ND7JR8HKZHJWDJQ",
+ "parent_id": null,
+ "user_id": null
+ }
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/samples/tel.json b/IoTDemoProxyGateway/samples/tel.json
new file mode 100644
index 0000000..b444b31
--- /dev/null
+++ b/IoTDemoProxyGateway/samples/tel.json
@@ -0,0 +1,71 @@
+{
+ "resourceMetrics": [
+ {
+ "resource": {
+ "attributes": [
+ {
+ "key": "service.name",
+ "value": {
+ "stringValue": "IoTDBJDBC"
+ }
+ },
+ {
+ "key": "iot.digital_twin.instance_id",
+ "value": {
+ "stringValue": "ocid1.iotdigitaltwininstance.oc1.uk-london-1.amaaaaaauevftmr7h6t75eifevhfihvbwazwi43ndgrauaawue22d6svdpna"
+ }
+ },
+ {
+ "key": "iot.digital_twin.model_id",
+ "value": {
+ "stringValue": "ocid1.iotdigitaltwinmodel.oc1.uk-london-1.amaaaaaauevftmr7wuslrtqim6zflcxhqaprmzib74p76ev64g4zqy7edbva"
+ }
+ },
+ {
+ "key": "iot.digital_twin.model_name",
+ "value": {
+ "stringValue": "homebattery"
+ }
+ }
+ ]
+ },
+ "scopeMetrics": [
+ {
+ "scope": {
+ "name": "com.oracle.demo.timg.iot.iotdbjdbc",
+ "version": "1.0.0"
+ },
+ "metrics": [
+ {
+ "name": "iot.normalized.solar_generation",
+ "description": "IoT normalized data value",
+ "unit": "1",
+ "gauge": {
+ "dataPoints": [
+ {
+ "attributes": [
+ {
+ "key": "iot.content.path",
+ "value": {
+ "stringValue": "SolarGeneration"
+ }
+ },
+ {
+ "key": "iot.content.type",
+ "value": {
+ "stringValue": "DECIMAL"
+ }
+ }
+ ],
+ "timeUnixNano": "1783071513549000000",
+ "asDouble": 495.0
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/Application.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/Application.java
new file mode 100644
index 0000000..1a7b3ed
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/Application.java
@@ -0,0 +1,46 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway;
+
+import io.micronaut.runtime.Micronaut;
+
+public class Application {
+
+ public static void main(String[] args) {
+ Micronaut.run(Application.class, args);
+ }
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/PropertyNames.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/PropertyNames.java
new file mode 100644
index 0000000..6f3e0fa
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/PropertyNames.java
@@ -0,0 +1,78 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway;
+
+public class PropertyNames {
+
+ public final static String MQTT_CLIENT_ENABLED = "mqtt.enabled";
+ public final static String MQTT_CLIENT_DEVICE_ID = "mqtt.client.client-id";
+ public final static String MQTT_CLIENT_USERNAME = "mqtt.client.user-name";
+ public final static String MQTT_CLIENT_PASSWORD = "mqtt.client.password";
+ public final static String MQTT_CLIENT_SERVER_URI = "mqtt.client.server-uri";
+
+ public final static String GATEWAY = "gateway";
+ public final static String GATEWAY_IDENTITY = GATEWAY + ".identity";
+ public final static String GATEWAY_DEVICE_NAME = GATEWAY_IDENTITY + ".name";
+ public final static String GATEWAY_DEVICE_KEY = GATEWAY_IDENTITY + ".devicekey";
+ public final static String GATEWAY_ENDPOINT = GATEWAY + ".endpoint";
+ public final static String GATEWAY_BASE_ENDPOINT = GATEWAY_ENDPOINT + ".base";
+ public final static String GATEWAY_STATS_ENDPOINT = GATEWAY_ENDPOINT + ".gatewaystats";
+ public final static String GATEWAY_CONFIG_ENDPOINT = GATEWAY_ENDPOINT + ".gatewayconfig";
+ public final static String GATEWAY_ENTITIES_ENDPOINT = GATEWAY_ENDPOINT + ".entities";
+
+ public final static String GATEWAY_CONFIG = GATEWAY + ".config";
+ public final static String GATEWAY_CONFIG_PUBLISH_ENABLED = GATEWAY_CONFIG + ".enabled";
+ public final static String GATEWAY_CONFIG_PUBLISH_RATE = GATEWAY_CONFIG + ".publishrate";
+ public final static String GATEWAY_CONFIG_INITIAL_DELAY = GATEWAY_CONFIG + ".initialdelay";
+ public final static String GATEWAY_STATS = GATEWAY + ".stats";
+ public final static String GATEWAY_STATS_PUBLISH_ENABLED = GATEWAY_STATS + ".enabled";
+ public final static String GATEWAY_STATS_PUBLISH_RATE = GATEWAY_STATS + ".publishrate";
+ public final static String GATEWAY_STATS_INITIAL_DELAY = GATEWAY_STATS + ".initialdelay";
+ public final static String GATEWAY_STATS_SUCESSFULL_RETRIEVE_WINDOW = GATEWAY_STATS + ".sucessfullretrievewindow";
+ public final static String GATEWAY_STATS_FAILED_RETRIEVE_WINDOW = GATEWAY_STATS + ".failedretrievewindow";
+ public final static String GATEWAY_STATS_SUCESSFULL_UPLOAD_WINDOW = GATEWAY_STATS + ".sucessfulluploadwindow";
+ public final static String GATEWAY_STATS_FAILED_UPLOAD_WINDOW = GATEWAY_STATS + ".faileduploadwindow";
+
+ public final static String HOME_ASSISTANT = "home-assistant";
+ public final static String HOME_ASSISTANT_MONITORED_ENTITIES_LIST = HOME_ASSISTANT + ".monitored-entities";
+ public final static String HOME_ASSISTANT_API = HOME_ASSISTANT + ".api";
+ public final static String HOME_ASSISTANT_API_AUTH_TOKEN = HOME_ASSISTANT_API + ".auth_token";
+ public final static String HOME_ASSISTANT_API_PROTOCOL = HOME_ASSISTANT_API + ".protocol";
+ public final static String HOME_ASSISTANT_API_HOSTNAME = HOME_ASSISTANT_API + ".host";
+ public final static String HOME_ASSISTANT_API_PORT = HOME_ASSISTANT_API + ".port";
+ public final static String HOME_ASSISTANT_API_URL = HOME_ASSISTANT_API + ".url";
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayCallTracker.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayCallTracker.java
new file mode 100644
index 0000000..8d89b64
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayCallTracker.java
@@ -0,0 +1,106 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+
+public class GatewayCallTracker {
+ private static final Duration DEFAULT_TRACKING_WINDOW = Duration.ofMinutes(10);
+
+ private final Clock clock;
+ private final Duration trackingWindow;
+ private final Deque callTimes = new ArrayDeque<>();
+
+ public GatewayCallTracker() {
+ this(DEFAULT_TRACKING_WINDOW);
+ }
+
+ public GatewayCallTracker(Duration trackingWindow) {
+ this(trackingWindow, Clock.systemUTC());
+ }
+
+ GatewayCallTracker(Duration trackingWindow, Clock clock) {
+ this.trackingWindow = validateTrackingWindow(trackingWindow);
+ this.clock = Objects.requireNonNull(clock, "clock");
+ }
+
+ public synchronized void trackCalls() {
+ callTimes.addLast(clock.instant());
+ }
+
+ public synchronized int callCount() {
+ pruneCallsBefore(clock.instant());
+ return callTimes.size();
+ }
+
+ public synchronized double averageCalls(long periodSeconds) {
+ validatePeriodSeconds(periodSeconds);
+ pruneCallsBefore(clock.instant());
+
+ double windowSeconds = trackingWindow.getSeconds() + trackingWindow.getNano() / 1_000_000_000.0;
+ return callTimes.size() * periodSeconds / windowSeconds;
+ }
+
+ private void pruneCallsBefore(Instant now) {
+ Instant cutoff = now.minus(trackingWindow);
+
+ while (!callTimes.isEmpty() && callTimes.peekFirst().isBefore(cutoff)) {
+ callTimes.removeFirst();
+ }
+ }
+
+ private static Duration validateTrackingWindow(Duration trackingWindow) {
+ Objects.requireNonNull(trackingWindow, "trackingWindow");
+
+ if (trackingWindow.isZero() || trackingWindow.isNegative()) {
+ throw new IllegalArgumentException("trackingWindow must be greater than zero");
+ }
+
+ return trackingWindow;
+ }
+
+ private static void validatePeriodSeconds(long periodSeconds) {
+ if (periodSeconds <= 0) {
+ throw new IllegalArgumentException("periodSeconds must be greater than zero");
+ }
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigData.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigData.java
new file mode 100644
index 0000000..e3debda
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigData.java
@@ -0,0 +1,51 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Serdeable
+@Builder
+public class GatewayConfigData {
+ private Long successfullharetrievetimewindow;
+ private Long failedharetrievetimewindow;
+ private Long successfulluploadtimewindow;
+ private Long faileduploadtimewindow;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigDataUploader.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigDataUploader.java
new file mode 100644
index 0000000..969caa1
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayConfigDataUploader.java
@@ -0,0 +1,102 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import java.util.Optional;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTGatewayConfigData;
+import com.oracle.demo.timg.iot.iotproxygateway.mqtt.MqttGatewayEventPublisher;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.annotation.ExecuteOn;
+import io.micronaut.scheduling.annotation.Scheduled;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import lombok.extern.java.Log;
+
+@Requires(property = PropertyNames.GATEWAY_CONFIG_PUBLISH_ENABLED, value = "true", defaultValue = "true")
+@Singleton
+@Log
+public class GatewayConfigDataUploader {
+ @Inject
+ private GatewayStats gatewayStats;
+ private final MqttGatewayEventPublisher gatewayEventPublisher;
+
+ @Inject
+ public GatewayConfigDataUploader(Optional gatewayEventPublisherOpt) {
+ if (gatewayEventPublisherOpt.isEmpty()) {
+ log.warning("gatewayEventPublisher not found, gateway config data will not be uploaded");
+ gatewayEventPublisher = null;
+ return;
+ } else {
+ gatewayEventPublisher = gatewayEventPublisherOpt.get();
+ }
+ }
+
+ @PostConstruct
+ void postConstruct() {
+ log.info("GatewayConfigDataUploader starting operation");
+ }
+
+ /*
+ * have this use the micronaut scheduler, it's easier
+ */
+ @Scheduled(fixedRate = "${" + PropertyNames.GATEWAY_CONFIG + ":1200s}", initialDelay = "${"
+ + PropertyNames.GATEWAY_CONFIG_INITIAL_DELAY + ":5s}")
+ @ExecuteOn(TaskExecutors.IO)
+ public void processConfiguration() {
+ // in theory this should not happen unless someone calls the method directly,
+ // but let's do some defensive programming
+ if (gatewayEventPublisher == null) {
+ return;
+ }
+ GatewayConfigData gatewayConfigData = gatewayStats.getGatewayConfigData();
+ IoTGatewayConfigData ioTGatewayConfigData = IoTGatewayConfigData.builder().payload(gatewayConfigData).build();
+ log.fine(() -> "Publishing gateway config " + ioTGatewayConfigData);
+ try {
+ gatewayEventPublisher.publishGatewayConfig(ioTGatewayConfigData);
+ gatewayStats.trackSucessfullUploadCall();
+ } catch (Exception e) {
+ log.warning("Exception uploading gateway config " + ioTGatewayConfigData + ", " + e.getLocalizedMessage());
+ gatewayStats.trackFailedUploadCall();
+ }
+
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStats.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStats.java
new file mode 100644
index 0000000..65b13bd
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStats.java
@@ -0,0 +1,106 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import java.time.Duration;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+
+import io.micronaut.context.annotation.Property;
+import jakarta.inject.Singleton;
+import lombok.Data;
+import lombok.extern.java.Log;
+
+@Data
+@Log
+@Singleton
+public class GatewayStats {
+ private GatewayCallTracker sucessfullHARetrieveCalls;
+ private GatewayCallTracker failedHARetrieveCalls;
+ private GatewayCallTracker sucessfullUploadCalls;
+ private GatewayCallTracker failedUploadCalls;
+ private long sucessfullHARetrieveWindowSeconds;
+ private long failedHARetrieveWindowSeconds;
+ private long sucessfullUploadWindowSeconds;
+ private long failedUploadWindowSeconds;
+
+ public GatewayStats(
+ @Property(name = PropertyNames.GATEWAY_STATS_SUCESSFULL_RETRIEVE_WINDOW, defaultValue = "PT10m") Duration sucessfullHARetrieveWindow,
+ @Property(name = PropertyNames.GATEWAY_STATS_FAILED_RETRIEVE_WINDOW, defaultValue = "PT10m") Duration failedHARetrieveWindow,
+ @Property(name = PropertyNames.GATEWAY_STATS_SUCESSFULL_UPLOAD_WINDOW, defaultValue = "PT10m") Duration sucessfullUploadWindow,
+ @Property(name = PropertyNames.GATEWAY_STATS_FAILED_UPLOAD_WINDOW, defaultValue = "PT10m") Duration failedUploadWindow) {
+ this.sucessfullHARetrieveCalls = new GatewayCallTracker(sucessfullHARetrieveWindow);
+ this.failedHARetrieveCalls = new GatewayCallTracker(failedHARetrieveWindow);
+ this.sucessfullUploadCalls = new GatewayCallTracker(sucessfullUploadWindow);
+ this.failedUploadCalls = new GatewayCallTracker(failedUploadWindow);
+ this.sucessfullHARetrieveWindowSeconds = sucessfullHARetrieveWindow.getSeconds();
+ this.failedHARetrieveWindowSeconds = failedHARetrieveWindow.getSeconds();
+ this.sucessfullUploadWindowSeconds = sucessfullUploadWindow.getSeconds();
+ this.failedUploadWindowSeconds = failedUploadWindow.getSeconds();
+ }
+
+ public void trackSucessfullHARetrieveCall() {
+ sucessfullHARetrieveCalls.trackCalls();
+ }
+
+ public void trackFailedHARetrieveCall() {
+ failedHARetrieveCalls.trackCalls();
+ }
+
+ public void trackSucessfullUploadCall() {
+ sucessfullUploadCalls.trackCalls();
+ }
+
+ public void trackFailedUploadCall() {
+ failedUploadCalls.trackCalls();
+ }
+
+ public GatewayStatsData getGatewayStatsData() {
+ return GatewayStatsData.builder()
+ .haretrievesuccess(sucessfullHARetrieveCalls.averageCalls(sucessfullHARetrieveWindowSeconds))
+ .haretrievefail(failedHARetrieveCalls.averageCalls(failedHARetrieveWindowSeconds))
+ .uploadsuccess(sucessfullUploadCalls.averageCalls(sucessfullUploadWindowSeconds))
+ .uploadfail(failedUploadCalls.averageCalls(failedUploadWindowSeconds)).build();
+ }
+
+ public GatewayConfigData getGatewayConfigData() {
+ return GatewayConfigData.builder().successfullharetrievetimewindow(sucessfullHARetrieveWindowSeconds)
+ .failedharetrievetimewindow(failedHARetrieveWindowSeconds)
+ .successfulluploadtimewindow(sucessfullUploadWindowSeconds)
+ .faileduploadtimewindow(failedUploadWindowSeconds).build();
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsData.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsData.java
new file mode 100644
index 0000000..5db4214
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsData.java
@@ -0,0 +1,51 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Serdeable
+@Builder
+public class GatewayStatsData {
+ private Double haretrievesuccess;
+ private Double haretrievefail;
+ private Double uploadsuccess;
+ private Double uploadfail;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsDataUploader.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsDataUploader.java
new file mode 100644
index 0000000..4eccdda
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/gateway/GatewayStatsDataUploader.java
@@ -0,0 +1,102 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.gateway;
+
+import java.util.Optional;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTGatewayStatsData;
+import com.oracle.demo.timg.iot.iotproxygateway.mqtt.MqttGatewayEventPublisher;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.annotation.ExecuteOn;
+import io.micronaut.scheduling.annotation.Scheduled;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import lombok.extern.java.Log;
+
+@Requires(property = PropertyNames.GATEWAY_STATS_PUBLISH_ENABLED, value = "true", defaultValue = "true")
+@Singleton
+@Log
+public class GatewayStatsDataUploader {
+ @Inject
+ private GatewayStats gatewayStats;
+ private MqttGatewayEventPublisher gatewayEventPublisher;
+
+ @Inject
+ public GatewayStatsDataUploader(Optional gatewayEventPublisherOpt) {
+ if (gatewayEventPublisherOpt.isEmpty()) {
+ log.warning("gatewayEventPublisher not found, gateway stats data will not be uploaded");
+ gatewayEventPublisher = null;
+ return;
+ } else {
+ gatewayEventPublisher = gatewayEventPublisherOpt.get();
+ }
+ }
+
+ @PostConstruct
+ void postConstruct() {
+ log.info("GatewayConfigDataUploader starting operation");
+ }
+
+ /*
+ * have this use the micronaut scheduler, it's easier
+ */
+ @Scheduled(fixedRate = "${" + PropertyNames.GATEWAY_STATS_PUBLISH_RATE + ":120s}", initialDelay = "${"
+ + PropertyNames.GATEWAY_STATS_INITIAL_DELAY + ":30s}")
+ @ExecuteOn(TaskExecutors.IO)
+ public void processStats() {
+ // in theory this should not happen unless someone calls the method directly,
+ // but let's do some defensive programming
+ if (gatewayEventPublisher == null) {
+ return;
+ }
+ GatewayStatsData gatewayStatsData = gatewayStats.getGatewayStatsData();
+ IoTGatewayStatsData ioTGatewayStatsData = IoTGatewayStatsData.builder().payload(gatewayStatsData).build();
+ log.fine(() -> "Publishing gateway stats " + ioTGatewayStatsData);
+ try {
+ gatewayEventPublisher.publishGatewayStats(ioTGatewayStatsData);
+ gatewayStats.trackSucessfullUploadCall();
+ } catch (Exception e) {
+ log.warning("Exception uploading gateway stats " + ioTGatewayStatsData + ", " + e.getLocalizedMessage());
+ gatewayStats.trackFailedUploadCall();
+ }
+ }
+
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantHttpClient.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantHttpClient.java
new file mode 100644
index 0000000..f066599
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantHttpClient.java
@@ -0,0 +1,58 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import static io.micronaut.http.HttpHeaders.USER_AGENT;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Header;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.http.client.exceptions.HttpClientException;
+
+@Client(id = "homeassistant", value = "${" + PropertyNames.HOME_ASSISTANT_API_URL + "}", path = "/api")
+@Header(name = USER_AGENT, value = "Micronaut HTTP Client")
+@Requires(property = PropertyNames.HOME_ASSISTANT_API_AUTH_TOKEN)
+public interface HomeAssistantHttpClient {
+ @Get("/states/{entityid}")
+ // @Error(exception = ReadTimeoutException.class)
+ public String getState(@PathVariable(name = "entityid") String entityid) throws HttpClientException;
+
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntity.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntity.java
new file mode 100644
index 0000000..c5a6750
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntity.java
@@ -0,0 +1,61 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTType;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.java.Log;
+
+@Data
+@NoArgsConstructor
+@Log
+@Serdeable
+public class HomeAssistantMonitoredEntity {
+ private String name;
+ private String entityid;
+ private IoTType iottype;
+ private String fieldname;
+ private SendMode sendmode = SendMode.ALWAYS; // default to always as it's the broadest option
+ private boolean dontsendifunavailable = true;
+
+ public boolean missingFields() {
+ return (name == null) || (entityid == null) || (iottype == null);
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntityManager.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntityManager.java
new file mode 100644
index 0000000..9c070a6
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntityManager.java
@@ -0,0 +1,98 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.stream.Collectors;
+
+import com.oracle.demo.timg.iot.iotproxygateway.gateway.GatewayStats;
+
+import io.micronaut.context.annotation.Context;
+import io.micronaut.context.event.ShutdownEvent;
+import io.micronaut.context.event.StartupEvent;
+import io.micronaut.runtime.event.annotation.EventListener;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.TaskScheduler;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+import lombok.ToString;
+import lombok.extern.java.Log;
+
+@Singleton
+@Log
+@Context
+public class HomeAssistantMonitoredEntityManager {
+ @ToString.Exclude
+ private final TaskScheduler taskScheduler;
+ @Inject
+ private Collection monitoredEntitySets;
+ @Inject
+ private GatewayStats gatewayStats;
+ private List> scheduledFutures;
+
+ public HomeAssistantMonitoredEntityManager(@Named(TaskExecutors.SCHEDULED) TaskScheduler taskScheduler) {
+ log.info("In startup monitor constructor");
+ this.taskScheduler = taskScheduler;
+ }
+
+ @PostConstruct
+ void postConstruct() {
+ log.fine("Startup has given us " + monitoredEntitySets.size() + " monitored entity sets which are "
+ + monitoredEntitySets);
+ }
+
+ @EventListener
+ public void startup(StartupEvent event) {
+ log.info("Starting scheduling of HA entity set monitoring");
+ scheduledFutures = monitoredEntitySets.stream().map(monitoredEntity -> {
+ log.fine(() -> "Starting monitored entity " + monitoredEntity);
+ ScheduledFuture> sf = taskScheduler.scheduleAtFixedRate(monitoredEntity.getInitaldelay(),
+ monitoredEntity.getRetrievalrate(), monitoredEntity);
+ log.info(() -> "Started monitored entity " + monitoredEntity);
+ return sf;
+ }).collect(Collectors.toList());
+ }
+
+ @EventListener
+ public void shutdown(ShutdownEvent event) {
+ scheduledFutures.stream().forEach(scheduledFuture -> scheduledFuture.cancel(false));
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntitySet.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntitySet.java
new file mode 100644
index 0000000..c89cfac
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantMonitoredEntitySet.java
@@ -0,0 +1,291 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+import com.oracle.demo.timg.iot.iotproxygateway.gateway.GatewayStats;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTEntityData;
+import com.oracle.demo.timg.iot.iotproxygateway.mqtt.MqttUploadHandler;
+
+import io.micronaut.context.annotation.EachProperty;
+import io.micronaut.context.annotation.Parameter;
+import io.micronaut.serde.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import lombok.extern.java.Log;
+
+@Data
+@NoArgsConstructor
+@Log
+@EachProperty(value = PropertyNames.HOME_ASSISTANT_MONITORED_ENTITIES_LIST, primary = "name", list = true)
+public class HomeAssistantMonitoredEntitySet implements Runnable {
+ // get the UTC TZ once to speed things later
+ private final static ZoneId UTC_TZ = ZoneId.of("UTC");
+ private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+ private final static ZonedDateTime EPOCH_TIME = Instant.EPOCH.atZone(UTC_TZ);
+ private final static HomeAssistantState EPOCH_HA_STATE = HomeAssistantState.builder().last_changed(EPOCH_TIME)
+ .last_reported(EPOCH_TIME).last_updated(EPOCH_TIME).build();
+ private String name;
+ private Boolean doupload;
+ private Duration initaldelay = Duration.ofSeconds(5);
+ private Duration retrievalrate = Duration.ofSeconds(10);
+ private String devicekey;
+ private String endpoint;
+ private TimestampMode timestampMode = TimestampMode.EARLIEST;
+ private List monitoredentities;
+ @ToString.Exclude
+ @Inject
+ private HomeAssistantHttpClient homeAssistantClient;
+ @ToString.Exclude
+ @Inject
+ private MqttUploadHandler mqttUploadHandler;
+ @ToString.Exclude
+ @Inject
+ private ObjectMapper mapper;
+ @ToString.Exclude
+ @Inject
+ private GatewayStats gatewayStats;
+ @ToString.Exclude
+ private Map laststates = new HashMap<>();
+
+ @Inject
+ public HomeAssistantMonitoredEntitySet(@Parameter String name) {
+ log.info("Monitored entity in constructor for " + name);
+ }
+
+ @PostConstruct
+ void postConstruct() {
+ log.fine("Configuring initial last states for monitored entity " + this.name);
+ // make sure that all of the entities are valid
+ String problemEntities = monitoredentities.stream().filter(entity -> entity.missingFields())
+ .map(entity -> this.name + "/" + entity.getName()).collect(Collectors.joining(","));
+ if ((problemEntities != null) && (problemEntities.length() > 0)) {
+ log.warning("Entity set " + this.getName() + " has incomplete entities " + problemEntities
+ + ". They will be removed from processing");
+ monitoredentities = monitoredentities.stream().filter(entity -> !entity.missingFields()).toList();
+ }
+ // for all of the states we are monitoring setup a last state entry so we have a
+ // known good start
+ monitoredentities.stream().forEach(entity -> laststates.put(entity.getName(), EPOCH_HA_STATE));
+ log.fine("constructed monitored entity " + this);
+ }
+
+ @Override
+ public void run() {
+ if (monitoredentities.isEmpty()) {
+ log.info("Monitored entity set " + name + " has no entities");
+ return;
+ }
+ log.finer("Running monitored entity set " + name);
+ Map payload = new HashMap<>();
+ ZonedDateTime observationTime = processEntities(payload);
+ if (payload.size() == 0) {
+ log.finer("Monitored entity set " + name + "payload has no data to upload, returning");
+ return;
+ }
+ // it shouldn't happen but just in case the observation time is null while there
+ // are payload entries apply a suitable default
+ if (observationTime == null) {
+ observationTime = ZonedDateTime.now(UTC_TZ);
+ }
+ // add the timestamp
+ payload.put(IoTEntityData.TIMESTAMP_FIELD_NAME, observationTime.format(formatter));
+ try {
+ log.finer("Uploading payload of HA state is " + payload);
+ mqttUploadHandler.upload(payload, this);
+ } catch (Exception e) {
+ log.info("Monitored entity set " + name + "Exception getting a state or other actions "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * @param payload
+ */
+ private ZonedDateTime processEntities(Map payload) {
+ // for each of the entities we are monitoring process it
+ // build a stream comparison to work out what the timeObserved should be
+ // the processEntiry will provide the field we're looking at
+ LongStream entityTimeAsLong = monitoredentities.stream().map(entity -> processEntity(payload, entity))
+ .filter(zdt -> zdt != null).mapToLong(zdt -> {
+ Instant instant = zdt.toInstant();
+ return (Long) Math.addExact(Math.multiplyExact(instant.getEpochSecond(), 1_000_000L),
+ instant.getNano() / 1_000);
+ });
+ // based on our comparison type calculate the actual microseconds value
+ Long sortedTsOpt = switch (timestampMode) {
+ case AVERAGE -> Math.round(entityTimeAsLong.average().orElse(0));
+ case EARLIEST -> entityTimeAsLong.min().orElse(0);
+ case LATEST -> entityTimeAsLong.max().orElse(0);
+ };
+ // if there is nothing there then it's reasonable to assume nothing was added
+ if (sortedTsOpt == 0) {
+ return null;
+ }
+ // try to work out that that means
+ long seconds = Math.floorDiv(sortedTsOpt, 1_000_000L);
+ long remainingMicros = Math.floorMod(sortedTsOpt, 1_000_000L);
+
+ Instant instant = Instant.ofEpochSecond(seconds, remainingMicros * 1_000L);
+ return instant.atZone(UTC_TZ);
+ }
+
+ private ZonedDateTime processEntity(Map payload, HomeAssistantMonitoredEntity entity) {
+ // get the last state, we know it must be there, but just in case
+ HomeAssistantState laststate = laststates.get(entity.getName());
+ if (laststate == null) {
+ laststate = createCurrentState(entity);
+ }
+ String stateString;
+ try {
+ stateString = homeAssistantClient.getState(entity.getEntityid());
+ } catch (Exception e) {
+ log.warning("Unable to get monitored entity " + this + " because " + e.getLocalizedMessage());
+ gatewayStats.trackFailedHARetrieveCall();
+ return null;
+ }
+ if (stateString == null) {
+ log.warning("Returned state of monitored entity " + this + " is null");
+ gatewayStats.trackFailedHARetrieveCall();
+ return null;
+ }
+ log.finer("Returned state string is :" + stateString);
+ HomeAssistantState state;
+ try {
+ state = mapper.readValue(stateString, HomeAssistantState.class);
+ } catch (Exception e) {
+ log.warning("Unable to de-serialize the state because " + e.getLocalizedMessage());
+ gatewayStats.trackFailedHARetrieveCall();
+ return null;
+ }
+ log.fine("Extracted state is " + state);
+ gatewayStats.trackSucessfullHARetrieveCall();
+ // make sure that we have the relevant times, even if we don't use them here
+ // they may be needed on another pass through
+ if (state.getLast_changed() == null) {
+ state.setLast_changed(ZonedDateTime.now(ZoneOffset.UTC));
+ }
+ if (state.getLast_reported() == null) {
+ state.setLast_reported(ZonedDateTime.now(ZoneOffset.UTC));
+ }
+ if (state.getLast_updated() == null) {
+ state.setLast_updated(ZonedDateTime.now(ZoneOffset.UTC));
+ }
+ // do we flag to send the data or not ?
+ // for now default to false
+ boolean proceedToSend = false;
+ switch (entity.getSendmode()) {
+ case ALWAYS: {
+ // Send the update each time we get data from home assistant
+ proceedToSend = true;
+ break;
+ }
+ case ON_REPORT: {
+ log.finer("Checking if there is a report, retrieved states last report time is "
+ + state.getLastReportedAsString() + ", previous is " + laststate.getLastReportedAsString());
+ // send when home assistant updates itself, this is regardless of if HA data has
+ // actually changed
+ if (state.getLast_reported().isAfter(laststate.getLast_reported())) {
+ proceedToSend = true;
+ }
+ break;
+ }
+ case ON_CHANGE: {
+ log.finer("Checking if there is a change, retrieved states last report time is "
+ + state.getLastChangedAsString() + ", previous is " + laststate.getLastChangedAsString());
+ // send when home assistant updates itself, this is regardless of if HA data has
+ // actually changed
+ if (state.getLast_changed().isAfter(laststate.getLast_changed())) {
+ proceedToSend = true;
+ }
+ break;
+ }
+ default:
+ log.severe("Unsupported sendmode found for HomeAssistantMonitoredEntity " + entity
+ + ", cannot process state " + stateString);
+ return null;
+ }
+ // switch the saved state so we have the current timestamp to compare to next
+ // time we get an new state from HomeAssistant
+ laststates.put(entity.getName(), state);
+ // are we adding this to the upload ?
+ if (proceedToSend) {
+ if (entity.isDontsendifunavailable() && state.isStateUnavailableOrMissing()) {
+ log.finer("donttsendifunavailable is true and state is null or unavailable");
+ // the state is not set or unavailable, and we are not sending in this case so
+ // don't send it
+ return null;
+ } else {
+ String fieldName = entity.getIottype().getFieldName(entity);
+ Object fieldValue = entity.getIottype().createObjectFrom(state);
+ payload.put(fieldName, fieldValue);
+ // we're sending, so return the last reported time, this will always be the
+ // setting we want because it it's ALWAYS that's the best we have, if it's
+ // ON)CHANGE then that will have been updated at the last report, and if it's
+ // on_report that's theupdate time even if it's not chenaged
+ return state.getLast_reported();
+ }
+ } else {
+ // if the entity doesn't pass the comparisons then its time stamps should not be
+ // part of the returned data
+ return null;
+ }
+ }
+
+ private HomeAssistantState createCurrentState(HomeAssistantMonitoredEntity entity) {
+ ZonedDateTime currentTime = ZonedDateTime.now(UTC_TZ);
+ HomeAssistantState currentState = HomeAssistantState.builder().last_changed(currentTime)
+ .last_reported(currentTime).last_updated(currentTime).build();
+ laststates.put(entity.getName(), currentState);
+ return currentState;
+ }
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantRequestFilter.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantRequestFilter.java
new file mode 100644
index 0000000..9c3addc
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantRequestFilter.java
@@ -0,0 +1,74 @@
+/*Copyright (c) 2025 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.MutableHttpRequest;
+import io.micronaut.http.annotation.ClientFilter;
+import io.micronaut.http.annotation.RequestFilter;
+import jakarta.inject.Inject;
+import lombok.extern.java.Log;
+
+@ClientFilter(patterns = "/api/**")
+@Requires(property = PropertyNames.HOME_ASSISTANT_API_AUTH_TOKEN)
+@Log
+public class HomeAssistantRequestFilter {
+
+ private String authToken;
+
+ @Inject
+ public HomeAssistantRequestFilter(@Property(name = PropertyNames.HOME_ASSISTANT_API_AUTH_TOKEN) String authToken) {
+ this.authToken = authToken;
+ }
+
+ @RequestFilter
+ public void doFilter(MutableHttpRequest> request) {
+ log.finer("Adding auth header ");
+ request.bearerAuth(authToken);
+ log.finer("Adding content type header ");
+ request.contentType(MediaType.APPLICATION_JSON_TYPE);
+ log.finer(() -> "request uri " + request.getUri().toASCIIString());
+ log.finer(() -> "request path " + request.getPath());
+ log.finer(() -> "request params = " + request.getParameters().asMap().toString());
+ log.finer(() -> "request headers = " + request.getHeaders().asMap().toString());
+ log.finer(() -> "Request body " + request.getBody(String.class).orElse("No body set"));
+ }
+}
\ No newline at end of file
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantState.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantState.java
new file mode 100644
index 0000000..79c4759
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/HomeAssistantState.java
@@ -0,0 +1,148 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+@Data
+@Serdeable
+@Builder
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class HomeAssistantState {
+ @JsonIgnore
+ public final static String STATE_UNAVAILABLE = "unavailable";
+ // get the UTC TZ once to speed things later
+ @JsonIgnore
+ private final static ZoneId UTCTZ = ZoneId.of("UTC");
+ @JsonIgnore
+ private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+
+ private String entity_id;
+ private String state;
+ private Map attributes;
+
+ @JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXX")
+ @Builder.Default
+ private ZonedDateTime last_changed = ZonedDateTime.now(UTCTZ);
+ @JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXX")
+ @Builder.Default
+ private ZonedDateTime last_reported = ZonedDateTime.now(UTCTZ);
+ @JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXX")
+ @Builder.Default
+ private ZonedDateTime last_updated = ZonedDateTime.now(UTCTZ);
+
+ private Map context;
+
+ public boolean isStateUnavailableOrMissing() {
+ return (state == null) || (state.equalsIgnoreCase(STATE_UNAVAILABLE));
+ }
+
+ public String getLastChangedAsString() {
+ return last_changed.format(formatter);
+ }
+
+ public String getLastReportedAsString() {
+ return last_reported.format(formatter);
+ }
+
+ public String getLastUpdatedAsString() {
+ return last_updated.format(formatter);
+ }
+
+ public Double getStateAsDouble(String defaultValue) {
+ if (state.equalsIgnoreCase(STATE_UNAVAILABLE)) {
+ return Double.valueOf(defaultValue);
+ } else {
+ return Double.valueOf(state);
+ }
+ }
+
+ public Double getStateAsDouble() {
+ return Double.valueOf(state);
+ }
+
+ public Integer getStateAsInteger(String defaultValue) {
+ if (state.equalsIgnoreCase(STATE_UNAVAILABLE)) {
+ return Integer.valueOf(defaultValue);
+ } else {
+ return Integer.valueOf(state);
+ }
+ }
+
+ public Integer getStateAsInteger() {
+ return Integer.valueOf(state);
+ }
+
+ public String getStateAsString() {
+ return state;
+ }
+
+ public final static String SWITCH_ON = "on";
+
+ public Boolean getStateStringMatchesValue(@NonNull String defaultValue, @NonNull String valueToReturnTrue) {
+ String stateValue = state;
+ if (state.equalsIgnoreCase(STATE_UNAVAILABLE)) {
+ stateValue = defaultValue;
+ }
+ return stateValue.equalsIgnoreCase(valueToReturnTrue);
+ }
+
+ public Boolean getStateAsBoolean(String defaultValue) {
+ if (state.equalsIgnoreCase(STATE_UNAVAILABLE)) {
+ return Boolean.valueOf(defaultValue);
+ } else {
+ return Boolean.valueOf(state);
+ }
+ }
+
+ public Boolean getStateAsBoolean() {
+ return Boolean.valueOf(state);
+ }
+
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/MissingEntitiesException.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/MissingEntitiesException.java
new file mode 100644
index 0000000..1a3b4fe
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/MissingEntitiesException.java
@@ -0,0 +1,44 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+import lombok.experimental.StandardException;
+
+@StandardException
+public class MissingEntitiesException extends Exception {
+ private static final long serialVersionUID = -2629852085814797150L;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/SendMode.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/SendMode.java
new file mode 100644
index 0000000..422669e
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/SendMode.java
@@ -0,0 +1,41 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+public enum SendMode {
+ ALWAYS, ON_CHANGE, ON_REPORT
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/TimestampMode.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/TimestampMode.java
new file mode 100644
index 0000000..6d994ab
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/homeassistantentities/TimestampMode.java
@@ -0,0 +1,41 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities;
+
+public enum TimestampMode {
+ EARLIEST, LATEST, AVERAGE
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTEntityData.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTEntityData.java
new file mode 100644
index 0000000..0242b02
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTEntityData.java
@@ -0,0 +1,57 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.iotdata;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@Serdeable
+@Builder
+public class IoTEntityData {
+ @ToString.Exclude
+ @JsonIgnore
+ public final static String TIMESTAMP_FIELD_NAME = "timestamp";
+ private String devicekey;
+ private Map payload;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayConfigData.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayConfigData.java
new file mode 100644
index 0000000..3884e42
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayConfigData.java
@@ -0,0 +1,71 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.iotdata;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.oracle.demo.timg.iot.iotproxygateway.gateway.GatewayConfigData;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@Serdeable
+@Builder
+public class IoTGatewayConfigData {
+ // get the UTC TZ once to speed things later
+ @ToString.Exclude
+ @JsonIgnore
+ private final static ZoneId utcTz = ZoneId.of("UTC");
+ // note that for Indirectly connected devices the timestamp must be within the
+ // payload sub object as the current configuration of the gateway envelope means
+ // that's all that's passed on to the indirectly connected devices
+ // for the telemetry on the gateway itself then we can get to the envelope
+ // attributes before the contentRoot is applied (if there is one and its not $)
+ // so we can if we want we can have the timestamp at the outer (envelope) level
+ // or within the payload,
+ @Builder.Default
+ @JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSXXX")
+ private ZonedDateTime timestamp = ZonedDateTime.now(utcTz);
+ private final String devicekey = null;
+ private GatewayConfigData payload;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayStatsData.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayStatsData.java
new file mode 100644
index 0000000..f647d09
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTGatewayStatsData.java
@@ -0,0 +1,71 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.iotdata;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.oracle.demo.timg.iot.iotproxygateway.gateway.GatewayStatsData;
+
+import io.micronaut.serde.annotation.Serdeable;
+import lombok.Builder;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@Serdeable
+@Builder
+public class IoTGatewayStatsData {
+ // get the UTC TZ once to speed things later
+ @ToString.Exclude
+ @JsonIgnore
+ private final static ZoneId utcTz = ZoneId.of("UTC");
+ // note that for Indirectly connected devices the timestamp must be within the
+ // payload sub object as the current configuration of the gateway envelope means
+ // that's all that's passed on to the indirectly connected devices
+ // for the telemetry on the gateway itself then we can get to the envelope
+ // attributes before the contentRoot is applied (if there is one and its not $)
+ // so we can if we want we can have the timestamp at the outer (envelope) level
+ // or within the payload,
+ @Builder.Default
+ @JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSXXX")
+ private ZonedDateTime timestamp = ZonedDateTime.now(utcTz);
+ private final String devicekey = null;
+ private GatewayStatsData payload;
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTType.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTType.java
new file mode 100644
index 0000000..5984125
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/iotdata/IoTType.java
@@ -0,0 +1,108 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.iotdata;
+
+import com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities.HomeAssistantMonitoredEntity;
+import com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities.HomeAssistantState;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.java.Log;
+
+@AllArgsConstructor
+@Log
+public enum IoTType {
+ AIR_PRESSURE("-1", "millibar"), BINARY_ON_OFF("off", "boolean", "on"), BOOLEAN("false", "boolean"),
+ ENERGY_KILO_WATT_HOURS("-999999", "kilowatthours"), ENERGY_WATT_HOURS("-999999", "watthours"),
+ LUMINANCE("-1", "lux"), MATTER_DOOR("Closed", "door", "Open"), MATTER_WINDOW("Closed", "window", "Open"),
+ PERCENT("0", "percent"), POWER_WATTS("-999999", "watts"), POWER_KILO_WATTS("-999999", "kilowatts"),
+ RELATIVE_HUMIDITY("-1", "humidity"), SWITCH("off", "switch", "On"), TEMPERATURE("-274", "temperature");
+
+ @Getter
+ @NonNull
+ private String unavailableDefault;
+
+ @Getter
+ @NonNull
+ private String defaultFieldName;
+
+ @Getter
+ private String valueToReturnTrue;
+
+ private IoTType(String unavailableDefault, String defaultFieldName) {
+ this.unavailableDefault = unavailableDefault;
+ this.defaultFieldName = defaultFieldName;
+ }
+
+ public String getFieldName(HomeAssistantMonitoredEntity entity) {
+ if (entity.getFieldname() == null) {
+ return defaultFieldName;
+ } else {
+ return entity.getFieldname();
+ }
+ }
+
+ public Object createObjectFrom(HomeAssistantState hastate) {
+ switch (this) {
+ case BINARY_ON_OFF:
+ case MATTER_DOOR:
+ case MATTER_WINDOW:
+ case SWITCH: {
+ return hastate.getStateStringMatchesValue(unavailableDefault, valueToReturnTrue);
+ }
+ case BOOLEAN: {
+ return hastate.getStateAsBoolean(unavailableDefault);
+ }
+ case AIR_PRESSURE:
+ case ENERGY_KILO_WATT_HOURS:
+ case ENERGY_WATT_HOURS:
+ case LUMINANCE:
+ case PERCENT:
+ case POWER_WATTS:
+ case POWER_KILO_WATTS:
+ case RELATIVE_HUMIDITY:
+ case TEMPERATURE: {
+ return hastate.getStateAsDouble(unavailableDefault);
+ }
+ default: {
+ throw new UnsupportedOperationException(
+ "Support for IoT type " + this + " is not yet implemented, cannot get the value for it");
+ }
+ }
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttGatewayEventPublisher.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttGatewayEventPublisher.java
new file mode 100644
index 0000000..a2d0e20
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttGatewayEventPublisher.java
@@ -0,0 +1,72 @@
+/*Copyright (c) 2025 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.mqtt;
+
+import java.util.concurrent.CompletableFuture;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTGatewayConfigData;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTGatewayStatsData;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.mqtt.annotation.Topic;
+import io.micronaut.mqtt.annotation.v5.MqttPublisher;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.annotation.ExecuteOn;
+import jakarta.inject.Singleton;
+
+@MqttPublisher
+@Singleton
+@Requires(property = PropertyNames.GATEWAY_DEVICE_NAME)
+@Requires(property = PropertyNames.MQTT_CLIENT_DEVICE_ID)
+@Requires(property = PropertyNames.MQTT_CLIENT_USERNAME)
+@Requires(property = PropertyNames.MQTT_CLIENT_PASSWORD)
+@Requires(property = PropertyNames.MQTT_CLIENT_SERVER_URI)
+@Requires(property = PropertyNames.MQTT_CLIENT_ENABLED, value = "true", defaultValue = "true")
+public interface MqttGatewayEventPublisher {
+ @Topic("${" + PropertyNames.GATEWAY_BASE_ENDPOINT + ":house/homeassistant}" + "/" + "${"
+ + PropertyNames.GATEWAY_STATS_ENDPOINT + ":gateway/stats}")
+ // @Topic("house/homeassistant/gateway/stats")
+ @ExecuteOn(TaskExecutors.IO)
+ public CompletableFuture publishGatewayStats(IoTGatewayStatsData data);
+
+ @Topic("${" + PropertyNames.GATEWAY_BASE_ENDPOINT + ":house/homeassistant}" + "/" + "${"
+ + PropertyNames.GATEWAY_CONFIG_ENDPOINT + ":gateway/config}")
+ // @Topic("house/homeassistant/gateway/config")
+ @ExecuteOn(TaskExecutors.IO)
+ public CompletableFuture publishGatewayConfig(IoTGatewayConfigData data);
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttHomeAssistantEntityPublisher.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttHomeAssistantEntityPublisher.java
new file mode 100644
index 0000000..485ee19
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttHomeAssistantEntityPublisher.java
@@ -0,0 +1,63 @@
+/*Copyright (c) 2025 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.mqtt;
+
+import java.util.concurrent.CompletableFuture;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.mqtt.annotation.Topic;
+import io.micronaut.mqtt.annotation.v5.MqttPublisher;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.annotation.ExecuteOn;
+import jakarta.inject.Singleton;
+
+@MqttPublisher
+@Singleton
+@Requires(property = PropertyNames.GATEWAY_DEVICE_NAME)
+@Requires(property = PropertyNames.MQTT_CLIENT_DEVICE_ID)
+@Requires(property = PropertyNames.MQTT_CLIENT_USERNAME)
+@Requires(property = PropertyNames.MQTT_CLIENT_PASSWORD)
+@Requires(property = PropertyNames.MQTT_CLIENT_SERVER_URI)
+@Requires(property = PropertyNames.MQTT_CLIENT_SERVER_URI)
+@Requires(property = PropertyNames.MQTT_CLIENT_ENABLED, value = "true", defaultValue = "true")
+public interface MqttHomeAssistantEntityPublisher {
+ // @Topic("house/homeassistant/entities")
+ @ExecuteOn(TaskExecutors.IO)
+ public CompletableFuture publishHomeAssistantData(@Topic String topic, String data);
+}
diff --git a/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttUploadHandler.java b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttUploadHandler.java
new file mode 100644
index 0000000..07d91c5
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/java/com/oracle/demo/timg/iot/iotproxygateway/mqtt/MqttUploadHandler.java
@@ -0,0 +1,119 @@
+/*Copyright (c) 2026 Oracle and/or its affiliates.
+
+The Universal Permissive License (UPL), Version 1.0
+
+Subject to the condition set forth below, permission is hereby granted to any
+person obtaining a copy of this software, associated documentation and/or data
+(collectively the "Software"), free of charge and under any and all copyright
+rights in the Software, and any and all patent rights owned or freely
+licensable by each licensor hereunder covering either (i) the unmodified
+Software as contributed to or provided by such licensor, or (ii) the Larger
+Works (as defined below), to deal in both
+
+(a) the Software, and
+(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+one is included with the Software (each a "Larger Work" to which the Software
+is contributed by such licensors),
+
+without restriction, including without limitation the rights to copy, create
+derivative works of, display, perform, and distribute the Software and make,
+use, sell, offer for sale, import, export, have made, and have sold the
+Software and the Larger Work(s), and to sublicense the foregoing rights on
+either these or other terms.
+
+This license is subject to the following condition:
+The above copyright notice and either this complete permission notice or at
+a minimum a reference to the UPL must be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+package com.oracle.demo.timg.iot.iotproxygateway.mqtt;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+
+import com.oracle.demo.timg.iot.iotproxygateway.PropertyNames;
+import com.oracle.demo.timg.iot.iotproxygateway.gateway.GatewayStats;
+import com.oracle.demo.timg.iot.iotproxygateway.homeassistantentities.HomeAssistantMonitoredEntitySet;
+import com.oracle.demo.timg.iot.iotproxygateway.iotdata.IoTEntityData;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.serde.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import lombok.ToString;
+import lombok.extern.java.Log;
+
+@Singleton
+@Log
+public class MqttUploadHandler {
+ @Inject
+ private GatewayStats gatewayStats;
+ private final String topicBase;
+ @ToString.Exclude
+ private final MqttHomeAssistantEntityPublisher mqttHomeAssistantEntityPublisher;
+ @ToString.Exclude
+ private final ObjectMapper mapper;
+
+ @Inject
+ public MqttUploadHandler(Optional mqttHomeAssistantEntityPublisherOptional,
+ ObjectMapper mapper,
+ @Property(name = PropertyNames.GATEWAY_BASE_ENDPOINT, defaultValue = "house/homeassistant") String endpointBase,
+ @Property(name = PropertyNames.GATEWAY_ENTITIES_ENDPOINT, defaultValue = "entities") String endpointEntities) {
+ log.info("Constructing MqttUploadHandler");
+ if (mqttHomeAssistantEntityPublisherOptional.isEmpty()) {
+ log.info("Uploads to MQTT are turned off " + PropertyNames.MQTT_CLIENT_ENABLED);
+ mqttHomeAssistantEntityPublisher = null;
+ } else {
+ this.mqttHomeAssistantEntityPublisher = mqttHomeAssistantEntityPublisherOptional.get();
+ }
+ this.mapper = mapper;
+ this.topicBase = endpointBase + "/" + endpointEntities;
+ }
+
+ @PostConstruct
+ void postConstruct() {
+ log.info("mqtt entity uploader configued with topic prefix " + topicBase);
+ }
+
+ public void upload(Map ioTCoreEvent, HomeAssistantMonitoredEntitySet entity) {
+ IoTEntityData ioTEntityData = IoTEntityData.builder().devicekey(entity.getDevicekey()).payload(ioTCoreEvent)
+ .build();
+ String mappedToJson;
+ try {
+ mappedToJson = mapper.writeValueAsString(ioTEntityData);
+ } catch (IOException e) {
+ log.severe("Error converting to Json, this should not have happened, " + e.getLocalizedMessage());
+ return;
+ }
+ String topic = topicBase + "/" + entity.getEndpoint();
+ log.info("Sending to topic " + topic + ", mapped event as json is " + mappedToJson);
+ // need to get the right topic details to match against the expected IoT topic
+
+ // try and sent it
+ if (entity.getDoupload()) {
+ try {
+ if (mqttHomeAssistantEntityPublisher != null) {
+ mqttHomeAssistantEntityPublisher.publishHomeAssistantData(topic, mappedToJson);
+ gatewayStats.trackSucessfullUploadCall();
+ } else {
+ log.info("No mqtt uploader set for entity " + entity.getName() + " for data " + mappedToJson);
+ }
+ } catch (Exception e) {
+ log.severe("Exception uploading event (" + mappedToJson + ") to Iot, " + e.getLocalizedMessage());
+ gatewayStats.trackFailedUploadCall();
+ }
+ } else {
+ log.info("Uploads disabled for entity " + entity.getName() + " for data " + mappedToJson);
+ }
+ }
+}
diff --git a/IoTDemoProxyGateway/src/main/resources/application.yml b/IoTDemoProxyGateway/src/main/resources/application.yml
new file mode 100644
index 0000000..d79f096
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+micronaut:
+ application:
+ name: ioTDemoProxyGateway
diff --git a/IoTDemoProxyGateway/src/main/resources/log4j2.xml b/IoTDemoProxyGateway/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..0839e48
--- /dev/null
+++ b/IoTDemoProxyGateway/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IoTSonnenUploader/DigitalTwin/Sample_SQL b/IoTSonnenUploader/DigitalTwin/Sample_SQL
index a61671e..453c057 100644
--- a/IoTSonnenUploader/DigitalTwin/Sample_SQL
+++ b/IoTSonnenUploader/DigitalTwin/Sample_SQL
@@ -9,4 +9,16 @@ FROM digital_twin_instances dti
INNER JOIN digital_twin_models dtm
ON JSON_VALUE(dti.data, '$.digitalTwinModelId' )
= JSON_VALUE(dtm.data, '$._id' )
-WHERE JSON_VALUE(dtm.data, '$.displayName' ) = 'homebattery'
\ No newline at end of file
+WHERE JSON_VALUE(dtm.data, '$.displayName' ) = 'homebattery'
+
+
+
+This will get the rejected data info in human readable form
+select ENDPOINT, TIME_RECEIVED, REASON_MESSAGE, UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(content, 4000, 1)) AS content_text from rejected_data order by TIME_RECEIVED DESC fetch first 10 rows only;
+
+this gets the most recent normalized data
+select * from SNAPSHOT_DATA order by TIME_OBSERVED desc fetch first 20 rows only ;
+
+
+this gets the most recent history data, best to ass a WHERE contentPath = 'path' clause though to focus on specific data items
+select * from HISTORIZED_DATA order by TIME_OBSERVED desc fetch first 10 rows only ;
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/.gitignore b/IoTSonnenUploader/GatewayProxy/.gitignore
new file mode 100644
index 0000000..b9c3449
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/.gitignore
@@ -0,0 +1 @@
+/GatewayTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/CreateEntities.sh b/IoTSonnenUploader/GatewayProxy/CreateEntities.sh
new file mode 100755
index 0000000..1392bda
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/CreateEntities.sh
@@ -0,0 +1,80 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+# run this in the GatewayProxy directory
+source ../OCISetup/common_names.sh
+source ./gateway_names.sh
+echo "Using OCI config profile $OCI_CLI_PROFILE"
+# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
+IOT_COMPARTMENT_OCID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
+
+
+export IOT_DOMAIN_GROUP_OCID=`oci iot domain-group list --display-name $IOT_DOMAIN_GROUP_NAME --compartment-id $IOT_COMPARTMENT_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_GROUP_OCID" ]]
+then
+ echo "Can not locate the domain group $IOT_DOMAIN_GROUP_NAME can not proceed"
+ exit -1
+else
+ echo "Iot Domain Group $IOT_DOMAIN_GROUP_NAME located"
+fi
+
+# get the data host details
+export IOT_DOMAIN_GROUP_DATA_HOST=`oci iot domain-group get --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r '.data."data-host"'`
+export IOT_DOMAIN_GROUP_SHORT_ID=`echo $IOT_DOMAIN_GROUP_DATA_HOST| tr '.' ' ' | awk '{print $1}'`
+
+export IOT_DOMAIN_OCID=`oci iot domain list --display-name $IOT_DOMAIN_NAME --compartment-id $IOT_COMPARTMENT_OCID --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_OCID" ]]
+then
+ echo "Can not locate IotDomain $IOT_DOMAIN_NAME can not proceed"
+ exit 2
+else
+ echo "Iot Domain $IOT_DOMAIN_NAME located"
+fi
+
+#Get the host details we will need
+export IOT_DOMAIN_HOST=`oci iot domain get --iot-domain-id $IOT_DOMAIN_OCID | jq -r '.data."device-host"'`
+export IOT_DOMAIN_SHORT_ID=`echo $IOT_DOMAIN_HOST| tr '.' ' ' | awk '{print $1}'`
+
+export GATEWAY_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$GATEWAY_OCID" ]]
+then
+ echo "No existing gateway named $GATEWAY_DISPLAY_NAME found, cannot continue"
+ exit -1
+else
+ echo "Located existing gateway named $GATEWAY_DISPLAY_NAME"
+fi
+# try and get the gateway credentials
+export GATEWAY_EXTERNAL_KEY=`oci iot digital-twin-instance get --digital-twin-instance-id $GATEWAY_OCID | jq -r '.data."external-key"'`
+export GATEWAY_SECRET_OCID=`oci iot digital-twin-instance get --digital-twin-instance-id $GATEWAY_OCID | jq -r '.data."auth-id"'`
+export GATEWAY_INSTANCE_SECRET_BASE64=`oci secrets secret-bundle get --secret-id $GATEWAY_SECRET_OCID --stage CURRENT | jq -r '.data."secret-bundle-content".content'`
+export GATEWAY_INSTANCE_SECRET=`echo $GATEWAY_INSTANCE_SECRET_BASE64 | base64 --decode`
+export GATEWAY_CREDENTIALS="$GATEWAY_EXTERNAL_KEY":"$GATEWAY_INSTANCE_SECRET"
+echo "GATEWAY credentials $GATEWAY_CREDENTIALS"
+
+echo "Creating entity models and instances"
+# we have setup everything, now we need to go through the entities in the entities folder creating the models and adapters
+pushd $GATEWAY_ENTITIES_DIRECTORY > /dev/null
+# Load the entities specific settings
+source ./entity_names.sh
+for entitydir in *"$ENTITY_DIRECTORY_SUFFIX"; do
+ [ -d "$entitydir" ] || continue
+ echo "Entities directory $entitydir"
+ pushd $entitydir > /dev/null
+ # make sure that the entries exist to create the model and adapter
+ if compgen -G "*${ENTITY_MODEL_SUFFIX}" > /dev/null \
+ && compgen -G "*${ENTITY_ROUTES_SUFFIX}" > /dev/null \
+ && compgen -G "*${ENTITY_ENVELOPE_SUFFIX}" > /dev/null \
+ && compgen -G "*${ENTITY_DATA_FILE_SUFFIX}" > /dev/null; then
+ echo "All required files exist for entity creation in model directory $entitydir"
+ # lets run it
+ export entitydir
+ ../CreateEntity.sh
+ else
+ echo "Missing one or more required files from Model ($ENTITY_MODEL_SUFFIX), Routes ($ENTITY_ROUTES_SUFFIX), Envolope ($ENTITY_ENVELOPE_SUFFIX), or Config data ($ENTITY_DATA_FILE_SUFFIX)"
+ echo "Cannot create entities in directory $entitydir"
+ fi
+ popd > /dev/null
+done
+# final move out back to the starting directory
+popd > /dev/null
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/CreateGateway.sh b/IoTSonnenUploader/GatewayProxy/CreateGateway.sh
new file mode 100755
index 0000000..bbfbb97
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/CreateGateway.sh
@@ -0,0 +1,148 @@
+# run this in the GatewayProxy directory
+source ../OCISetup/common_names.sh
+source ./gateway_names.sh
+echo "Using OCI config profile $OCI_CLI_PROFILE"
+# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
+IOT_COMPARTMENT_OCID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
+
+
+export IOT_DOMAIN_GROUP_OCID=`oci iot domain-group list --display-name $IOT_DOMAIN_GROUP_NAME --compartment-id $IOT_COMPARTMENT_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_GROUP_OCID" ]]
+then
+ echo "Can not locate the domain group $IOT_DOMAIN_GROUP_NAME can not proceed"
+ exit -1
+else
+ echo "Iot Domain Group $IOT_DOMAIN_GROUP_NAME located"
+fi
+
+# get the data host details
+export IOT_DOMAIN_GROUP_DATA_HOST=`oci iot domain-group get --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r '.data."data-host"'`
+export IOT_DOMAIN_GROUP_SHORT_ID=`echo $IOT_DOMAIN_GROUP_DATA_HOST| tr '.' ' ' | awk '{print $1}'`
+
+export IOT_DOMAIN_OCID=`oci iot domain list --display-name $IOT_DOMAIN_NAME --compartment-id $IOT_COMPARTMENT_OCID --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_OCID" ]]
+then
+ echo "Can not locate IotDomain $IOT_DOMAIN_NAME can not proceed"
+ exit 2
+else
+ echo "Iot Domain $IOT_DOMAIN_NAME located"
+fi
+
+#Get the host details we will need
+export IOT_DOMAIN_HOST=`oci iot domain get --iot-domain-id $IOT_DOMAIN_OCID | jq -r '.data."device-host"'`
+export IOT_DOMAIN_SHORT_ID=`echo $IOT_DOMAIN_HOST| tr '.' ' ' | awk '{print $1}'`
+
+export GATEWAY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$GATEWAY_MODEL_ID" ]]
+then
+ echo Creating Digital Twin Model for gateway $GATEWAY_MODEL_NAME
+ # Create the digtal twin model for the generic home battery, run from within the DigitalTwin folder
+ echo oci iot digital-twin-model create --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_MODEL_NAME --spec $GATEWAY_MODEL_FILE --wait-for-state ACTIVE
+ oci iot digital-twin-model create --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_MODEL_NAME --spec $GATEWAY_MODEL_FILE --wait-for-state ACTIVE
+ # get the id of the model you just created
+ export GATEWAY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ echo Created Digital Twin Gateway Model $GATEWAY_MODEL_NAME
+else
+ echo "Digital twin model for gateway $GATEWAY_MODEL_NAME already exists"
+fi
+
+export GATEWAY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$GATEWAY_ADAPTER_OCID" ]]
+then
+ echo Creating Digital Twin adapter $GATEWAY_ADAPTER_NAME
+ # create the adaptor using the multiple routes file.
+ # note that as the file contents is inlined into the JSON request then the structure of the JSON files MUST be valid as JSON and also meeting the spec - if not you will get missing param errors, even though you have specified all of the actual flags correctly
+ echo oci iot digital-twin-adapter create --iot-domain-id $IOT_DOMAIN_OCID --digital-twin-model-id $GATEWAY_MODEL_ID --inbound-routes $GATEWAY_ADAPTOR_ROUTE_MAPPINGS_FILE --display-name $GATEWAY_ADAPTER_NAME --inbound-envelope $GATEWAY_ADAPTOR_ENVELOPE_MAPPINGS_FILE --wait-for-state ACTIVE
+ oci iot digital-twin-adapter create --iot-domain-id $IOT_DOMAIN_OCID --digital-twin-model-id $GATEWAY_MODEL_ID --inbound-routes $GATEWAY_ADAPTOR_ROUTE_MAPPINGS_FILE --display-name $GATEWAY_ADAPTER_NAME --inbound-envelope $GATEWAY_ADAPTOR_ENVELOPE_MAPPINGS_FILE --wait-for-state ACTIVE
+ # get the OCID of the new adaptor
+ export GATEWAY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ echo Created Gateway adapter $GATEWAY_ADAPTER_NAME
+else
+ echo "Gateway adapter $GATEWAY_ADAPTER_NAME already exists"
+fi
+
+export GATEWAY_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$GATEWAY_OCID" ]]
+then
+ echo "No existing gateway named $GATEWAY_DISPLAY_NAME will attempt to create it"
+ export VAULT_OCID=`oci kms management vault list --compartment-id $IOT_COMPARTMENT_OCID --all | jq -r ".data[] | select (.\"display-name\" == \"$VAULT_NAME\") | select (.\"lifecycle-state\" == \"ACTIVE\") | .id"`
+ if [[ -z "$VAULT_OCID" ]]
+ then
+ echo "Can't locate vault $VAULT_NAME unable to create a new gateway instance"
+ exit -1
+ fi
+ echo "Located vault $VAULT_NAME"
+ export FIXED_SECRET_OCID=`oci secrets secret-bundle get-secret-bundle-by-name --vault-id $VAULT_OCID --secret-name $FIXED_SECRET_NAME | jq -r '.data."secret-id"'`
+ if [[ -z "$FIXED_SECRET_OCID" ]]
+ then
+ echo "Can't locate secret $FIXED_SECRET_NAME in vault $VAULT_NAME unable to create a new gateway instance"
+ exit -1
+ else
+ echo "Located secret $FIXED_SECRET_NAME in vault $VAULT_NAME"
+ fi
+ echo oci iot digital-twin-instance create --iot-domain-id $IOT_DOMAIN_OCID --connectivity-type GATEWAY --display-name $GATEWAY_DISPLAY_NAME --auth-id $FIXED_SECRET_OCID --digital-twin-adapter-id $GATEWAY_ADAPTER_OCID --external-key $GATEWAY_NAME --wait-for-state ACTIVE
+ oci iot digital-twin-instance create --iot-domain-id $IOT_DOMAIN_OCID --connectivity-type GATEWAY --display-name $GATEWAY_DISPLAY_NAME --auth-id $FIXED_SECRET_OCID --digital-twin-adapter-id $GATEWAY_ADAPTER_OCID --external-key $GATEWAY_NAME --wait-for-state ACTIVE
+ export GATEWAY_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ echo "Create gateway instance"
+else
+ echo "Found existing gateway instance"
+fi
+if [[ -z "$GATEWAY_OCID" ]]
+then
+ echo "Could not locate the digital twin instance, it may not have been created, exiting"
+ exit -2
+fi
+
+echo "Getting digital twin instance crededntials"
+# get the external key and auth details
+export GATEWAY_EXTERNAL_KEY=`oci iot digital-twin-instance get --digital-twin-instance-id $GATEWAY_OCID | jq -r '.data."external-key"'`
+# if we just created the instance we have this, but not if we found an existing one
+export GATEWAY_SECRET_OCID=`oci iot digital-twin-instance get --digital-twin-instance-id $GATEWAY_OCID | jq -r '.data."auth-id"'`
+export GATEWAY_INSTANCE_SECRET_BASE64=`oci secrets secret-bundle get --secret-id $GATEWAY_SECRET_OCID --stage CURRENT | jq -r '.data."secret-bundle-content".content'`
+export GATEWAY_INSTANCE_SECRET=`echo $GATEWAY_INSTANCE_SECRET_BASE64 | base64 --decode`
+export GATEWAY_CREDENTIALS="$GATEWAY_EXTERNAL_KEY":"$GATEWAY_INSTANCE_SECRET"
+echo "GATEWAY credentials $GATEWAY_CREDENTIALS"
+echo
+echo "To send the gateway stats test data using https for device $GATEWAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+#echo 'export CURRENT_TS=`date +%s%N | cut -b1-13`'
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$GATEWAY_ENDPOINT_GATEWAY_STATS_PATH\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": {\\\"haretrievesuccess\\\": 24.6,\\\"haretrievefail\\\": 1.4,\\\"uploadsuccess\\\": 24.6,\\\"uploadfail\\\": 1.4}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\"
+echo
+echo "To send the gateway config test data using https for device $GATEWAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$GATEWAY_ENDPOINT_GATEWAY_CONFIG_PATH\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": {\\\"successfullharetrievetimewindow\\\": 14,\\\"failedharetrievetimewindow\\\": 1, \\\"successfulluploadtimewindow\\\": 12,\\\"faileduploadtimewindow\\\": 2}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\"
+echo
+echo "To send gateway stats test data for the configuration using mqttx for device $GATEWAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+echo mqttx pub -t $GATEWAY_ENDPOINT_GATEWAY_STATS_PATH -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": {\\\"haretrievesuccess\\\": 24.6,\\\"haretrievefail\\\": 1.4,\\\"uploadsuccess\\\": 24.6,\\\"uploadfail\\\": 1.4}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\"
+echo
+echo "To send gateway config test data for the configuration using mqttx for device $GATEWAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+echo mqttx pub -t $GATEWAY_ENDPOINT_GATEWAY_CONFIG_PATH -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": {\\\"successfullharetrievetimewindow\\\": 14,\\\"failedharetrievetimewindow\\\": 1, \\\"successfulluploadtimewindow\\\": 12,\\\"faileduploadtimewindow\\\": 2}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\"
+echo
+echo "To get the most recent normalized data for gateway $GATEWAY_NAME"
+echo "oci iot digital-twin-instance get-content --digital-twin-instance-id $GATEWAY_OCID"
+# not generate a file containing the test data commands
+echo -n > $GATEWAY_TEST_DATA_FILE
+echo "Gateway test data" >> $GATEWAY_TEST_DATA_FILE
+echo "GATEWAY credentials $GATEWAY_CREDENTIALS" >> $GATEWAY_TEST_DATA_FILE
+echo >> $GATEWAY_TEST_DATA_FILE
+echo "To send the gateway stats test data using https for device $GATEWAY_NAME" >> $GATEWAY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $GATEWAY_TEST_DATA_FILE
+#echo 'export CURRENT_TS=`date +%s%N | cut -b1-13`' >> $GATEWAY_TEST_DATA_FILE
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$GATEWAY_ENDPOINT_GATEWAY_STATS_PATH\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": {\\\"haretrievesuccess\\\": 24.6,\\\"haretrievefail\\\": 1.4,\\\"uploadsuccess\\\": 24.6,\\\"uploadfail\\\": 1.4}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\" >> $GATEWAY_TEST_DATA_FILE
+echo >> $GATEWAY_TEST_DATA_FILE
+echo "To send the gateway config test data using https for device $GATEWAY_NAME" >> $GATEWAY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $GATEWAY_TEST_DATA_FILE
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$GATEWAY_ENDPOINT_GATEWAY_CONFIG_PATH\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": {\\\"successfullharetrievetimewindow\\\": 14,\\\"failedharetrievetimewindow\\\": 1, \\\"successfulluploadtimewindow\\\": 12,\\\"faileduploadtimewindow\\\": 2}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\" >> $GATEWAY_TEST_DATA_FILE
+echo >> $GATEWAY_TEST_DATA_FILE
+echo "To send gateway stats test data for the configuration using mqttx for device $GATEWAY_NAME" >> $GATEWAY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $GATEWAY_TEST_DATA_FILE
+echo mqttx pub -t $GATEWAY_ENDPOINT_GATEWAY_STATS_PATH -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": {\\\"haretrievesuccess\\\": 24.6,\\\"haretrievefail\\\": 1.4,\\\"uploadsuccess\\\": 24.6,\\\"uploadfail\\\": 1.4}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\" >> $GATEWAY_TEST_DATA_FILE
+echo >> $GATEWAY_TEST_DATA_FILE
+echo "To send gateway config test data for the configuration using mqttx for device $GATEWAY_NAME" >> $GATEWAY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $GATEWAY_TEST_DATA_FILE
+echo mqttx pub -t $GATEWAY_ENDPOINT_GATEWAY_CONFIG_PATH -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": {\\\"successfullharetrievetimewindow\\\": 14,\\\"failedharetrievetimewindow\\\": 1, \\\"successfulluploadtimewindow\\\": 12,\\\"faileduploadtimewindow\\\": 2}, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}\" >> $GATEWAY_TEST_DATA_FILE
+echo >> $GATEWAY_TEST_DATA_FILE
+echo "To get the most recent normalized data for gateway $GATEWAY_NAME" >> $GATEWAY_TEST_DATA_FILE
+echo "oci iot digital-twin-instance get-content --digital-twin-instance-id $GATEWAY_OCID" >> $GATEWAY_TEST_DATA_FILE
diff --git a/IoTSonnenUploader/GatewayProxy/DeleteEntities.sh b/IoTSonnenUploader/GatewayProxy/DeleteEntities.sh
new file mode 100755
index 0000000..ba369ef
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/DeleteEntities.sh
@@ -0,0 +1,52 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+# run this in the GatewayProxy directory
+source ../OCISetup/common_names.sh
+source ./gateway_names.sh
+echo "Using OCI config profile $OCI_CLI_PROFILE"
+# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
+IOT_COMPARTMENT_OCID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
+
+
+export IOT_DOMAIN_GROUP_OCID=`oci iot domain-group list --display-name $IOT_DOMAIN_GROUP_NAME --compartment-id $IOT_COMPARTMENT_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_GROUP_OCID" ]]
+then
+ echo "Can not locate the domain group $IOT_DOMAIN_GROUP_NAME can not proceed"
+ exit -1
+else
+ echo "Iot Domain Group $IOT_DOMAIN_GROUP_NAME located"
+fi
+
+export IOT_DOMAIN_OCID=`oci iot domain list --display-name $IOT_DOMAIN_NAME --compartment-id $IOT_COMPARTMENT_OCID --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$IOT_DOMAIN_OCID" ]]
+then
+ echo "Can not locate IotDomain $IOT_DOMAIN_NAME can not proceed"
+ exit 2
+else
+ echo "Iot Domain $IOT_DOMAIN_NAME located"
+fi
+echo "Creating entity models and instances"
+# we have setup everything, now we need to go through the entities in the entities folder creating the models and adapters
+pushd $GATEWAY_ENTITIES_DIRECTORY > /dev/null
+# Load the entities specific settings
+source ./entity_names.sh
+for entitydir in *"$ENTITY_DIRECTORY_SUFFIX"; do
+ [ -d "$entitydir" ] || continue
+ echo "Entities directory $entitydir"
+ pushd $entitydir > /dev/null
+ # make sure that the entries exist to create the model and adapter
+ if compgen -G "*${ENTITY_DATA_FILE_SUFFIX}" > /dev/null; then
+ echo "All required files exist to process deletions in model directory $entitydir"
+ # lets run it
+ export entitydir
+ ../DeleteEntity.sh
+ else
+ echo "Missing Config data ($ENTITY_DATA_FILE_SUFFIX)"
+ echo "Cannot delete entities in directory $entitydir"
+ fi
+ popd > /dev/null
+done
+# final move out back to the starting directory
+popd > /dev/null
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/DeleteGateway.sh b/IoTSonnenUploader/GatewayProxy/DeleteGateway.sh
new file mode 100755
index 0000000..a1dbe8c
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/DeleteGateway.sh
@@ -0,0 +1,66 @@
+#!/bin/bash -f
+source ../OCISetup/common_names.sh
+source ./gateway_names.sh
+echo "Using OCI config profile $OCI_CLI_PROFILE"
+# change these names as required
+# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
+IOT_COMPARTMENT_OCID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
+VAULT_OCID=$(oci kms management vault list \
+ --compartment-id "$IOT_COMPARTMENT_OCID" \
+ --all \
+ --query "data[?\"display-name\"=='$VAULT_NAME'].id | [0]" \
+ --raw-output)
+
+
+echo Getting OCIDs
+export IOT_DOMAIN_GROUP_OCID=`oci iot domain-group list --display-name $IOT_DOMAIN_GROUP_NAME --compartment-id $IOT_COMPARTMENT_OCID | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+
+if [[ -n "$IOT_DOMAIN_GROUP_OCID" ]]
+then
+ echo "Located domain group $IOT_DOMAIN_GROUP_NAME, looking for domain"
+else
+ echo "Can't locate IOT domain group $IOT_DOMAIN_GROUP_NAME can't continue"
+ exit -1
+fi
+
+export IOT_DOMAIN_OCID=`oci iot domain list --display-name $IOT_DOMAIN_NAME --compartment-id $IOT_COMPARTMENT_OCID --iot-domain-group-id $IOT_DOMAIN_GROUP_OCID | jq -r 'first(.data.items[]| select (."lifecycle-state" == "ACTIVE" or ."lifecycle-state" == "FAILED") | ."id") // empty'`
+if [[ -n "$IOT_DOMAIN_OCID" ]]
+then
+ echo "Located domain $IOT_DOMAIN_NAME, looking for digital twin instance"
+else
+ echo "Can't locate IOT domain group $IOT_DOMAIN_NAME can't continue"
+ exit -2
+fi
+
+# try to locate and delete the instance
+export GATEWAY_INSTANCE_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -n "$GATEWAY_INSTANCE_OCID" ]]
+then
+ echo "Located gateway instance $GATEWAY_DISPLAY_NAME, deleting it"
+ oci iot digital-twin-instance delete --digital-twin-instance-id $GATEWAY_INSTANCE_OCID --force --wait-for-state DELETED
+ echo "Deleted gateway instance $GATEWAY_DISPLAY_NAME"
+else
+ echo "Can't locate gateway instance $GATEWAY_DISPLAY_NAME"
+fi
+export GATEWAY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -n "$GATEWAY_ADAPTER_OCID" ]]
+then
+ echo "Located gateway adapter $GATEWAY_ADAPTER_NAME, deleting it"
+ oci iot digital-twin-adapter delete --digital-twin-adapter-id $GATEWAY_ADAPTER_OCID --force --wait-for-state DELETED
+ echo "Deleted gateway adapter $GATEWAY_ADAPTER_NAME"
+else
+ echo "Can't locate gateway adapter $GATEWAY_ADAPTER_NAME"
+fi
+
+export GATEWAY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $GATEWAY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -n "$GATEWAY_MODEL_ID" ]]
+then
+ echo "Located digital twin gateway model $GATEWAY_MODEL_NAME, deleting it"
+ oci iot digital-twin-model delete --digital-twin-model-id $GATEWAY_MODEL_ID --force --wait-for-state DELETED
+ echo "Deleted digital twin gateway model $GATEWAY_MODEL_NAME"
+else
+ echo "Can't locate digital twin gateway model $GATEWAY_MODEL_NAME"
+fi
+# make sure its there, then delete it
+touch $GATEWAY_TEST_DATA_FILE
+rm $GATEWAY_TEST_DATA_FILE
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/GatewayDTML.json b/IoTSonnenUploader/GatewayProxy/GatewayDTML.json
new file mode 100644
index 0000000..0ab3093
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/GatewayDTML.json
@@ -0,0 +1,84 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:gateway;1",
+ "@type": "Interface",
+ "displayName": "HAGateway",
+ "description": "Digital twin model for the home assistant gateway",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "SucessfullHARetrievalsLastTenMins",
+ "name": "haretrievesuccess",
+ "schema": "double"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "FailedHARetrievalsLastTenMins",
+ "name": "haretrievefail",
+ "schema": "double"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "SucessfullUploadsLastTenMins",
+ "name": "uploadsuccess",
+ "schema": "double"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "FailedUploadsLastTenMins",
+ "name": "uploadfail",
+ "schema": "double"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "TimeWindowForHARetrieveSuccessStats",
+ "name": "successfullharetrievetimewindow",
+ "schema": "long"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "TimeWindowForFailStats",
+ "name": "failedharetrievetimewindow",
+ "schema": "long"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "TimeWindowForUploadSuccessStats",
+ "name": "successfulluploadtimewindow",
+ "schema": "long"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "TimeWindowForUploadFailedStats",
+ "name": "faileduploadtimewindow",
+ "schema": "long"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/GatewayProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/GatewayProxyEnvelope.json
new file mode 100644
index 0000000..1796e4b
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/GatewayProxyEnvelope.json
@@ -0,0 +1,21 @@
+{
+ "reference-endpoint": "house/homeassistant/gateway/*",
+ "reference-payload": {
+ "data": {
+ "timestamp": "2026-07-10T16:09:18.934Z",
+ "payload": {
+ "haretrievesuccess": 54.5,
+ "haretrievefail": 0.5,
+ "uploadsuccess": 53.5,
+ "uploadfail": 1.5
+ },
+ "devicekey": "abc"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.timestamp",
+ "target": "$.devicekey",
+ "contentRoot": "$.payload"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/GatewayProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/GatewayProxyRoutes.json
new file mode 100644
index 0000000..a3cf92c
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/GatewayProxyRoutes.json
@@ -0,0 +1,40 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"gateway\" and endpoint(4) == \"stats\"}",
+ "description": "Home Assistant gateway statistics",
+ "payload-mapping": {
+ "$.haretrievesuccess": "$.haretrievesuccess",
+ "$.haretrievefail": "$.haretrievefail",
+ "$.uploadsuccess": "$.uploadsuccess",
+ "$.uploadfail": "$.uploadfail"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "haretrievesuccess": 54.5,
+ "haretrievefail": 0.5,
+ "uploadsuccess": 53.5,
+ "uploadfail": 1.5
+ }
+ }
+ },
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"gateway\" and endpoint(4) == \"config\"}",
+ "description": "Home Assistant gateway configuration",
+ "payload-mapping": {
+ "$.successfullharetrievetimewindow": "$.successfullharetrievetimewindow",
+ "$.failedharetrievetimewindow": "$.failedharetrievetimewindow",
+ "$.successfulluploadtimewindow": "$.successfulluploadtimewindow",
+ "$.faileduploadtimewindow": "$.faileduploadtimewindow"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "successfullharetrievetimewindow": 10,
+ "failedharetrievetimewindow": 11,
+ "successfulluploadtimewindow": 12,
+ "faileduploadtimewindow": 13
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/IMPORTANT_GW_ADAPTER_NOTES.md b/IoTSonnenUploader/GatewayProxy/IMPORTANT_GW_ADAPTER_NOTES.md
new file mode 100644
index 0000000..fa0a249
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/IMPORTANT_GW_ADAPTER_NOTES.md
@@ -0,0 +1,24 @@
+#Gateway vs device adapters
+
+ICD = indirectly connected device
+
+Note that a gateway MUST have an adapter with an envelopeMapping as the system requires it to have a target and cpntentRoute in the envelopeMapping. if that's nto the case (or they are missing) then trying to create the gateway device will result in an error `Digital twin adapter [ocid1.iotdigitaltwinadapter.oc1.......] does not support gateway connectivity`
+
+The gateway MUST have a endpointMappings field with a target and contentRoot entries.
+
+The *target* entry must map to the external key of the ICD, or if the data is for the gateway itself it should map to null
+
+Note that currently the ONLY place you can determine the target / contentRoot is in the envelopeMapping, and there can only be one of these in the adapter (and of course only one adapter per device) Though mechanisms for multiple target / contenRoute based on the end point are being investigated (no commitments of course !)
+
+It's up to the gateway and adapter author how the target determines the external key, but suggestions are it can be in the incoming endpoint, or it could be a field in the incoming data
+
+If the target is non null (i.e. it points to the external key) then the message is "handed off" to the adapter for the digital twin instance with that external key. If it's null then it's handled by the adaptor for the gateway itself. Currently it's unclear at what point the endpointMappings for the ICD / GW are done, this may have an impact on things like setting timeObserved if using a non $ contentRoot (see later)
+
+If the target is null then the normal envelopeMappings are applied (e.g setting timeObserved if you need to, note that at this point the mappings are based on the entire input data (i.e. before contentRoot below) and it's then handed to the routes part of the gateway adapter. Unclear if setting timeObserved at this point will apply when it gets to the indirectly connected device of it the adaptors there will need to do that based on the data they get.
+
+The contentRoot is used to specify where in the gateway level input the actual content data exists, for example the gateway may have some meta data and then encapsulate the actual device data in a field (e.g $.payload) and you only want that payload to be handed to the ICD adapters OR the telemetry route handling for the gateway itself (i.e. when it is receiving data as a directly connected device)
+
+Using a non $ content root at the GW level may mean that normal "root" level data (e.g. timestamps) must be in the new content root when it gets to the ICDs.
+
+Gotchas
+Note that the endpoint *is not modified* when it's passed to the ICD adapter, so a gateway of home/ha/gateway/entities/lux arriving at the GW level will be the same when passed to the ICD level, so if using multiple routes then that full path needs to be considered (of course * would still work if there is only one route as the adapter level is tied to the device) This probably only matters if you're trying to use the same adapter to connect to the device model for both ICD and directly connected devices, in practice I suspect you'd not do that.
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/README.md b/IoTSonnenUploader/GatewayProxy/README.md
new file mode 100644
index 0000000..9a6190b
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/README.md
@@ -0,0 +1,8 @@
+#README
+the gateway proxy directory is used to hold configuration in formation for the IoTDemoProxyGateway project. this configuration information is held in this project (IoTSonnen), not IoTDemoProxyGateway because it's linked to the setup scripts used here, and cross project would result in duplication and sync problems.
+
+##How to use
+the gateway-names.sh script contains all of the names, check that to see if there are things you want to change
+
+You need to have setup the IoTCore, then run the CreateGatewayConfig.sh script,It will gather then various bits of info that the core has setup, then build on top of that
+
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureConfigData.sh
new file mode 100755
index 0000000..f3da732
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HATAirPressureModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAAirPressureAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=airpressure
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"airpressure\\\": 1013.25, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureDTML.json b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureDTML.json
new file mode 100644
index 0000000..e801c1f
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:airpressure;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Pressure",
+ "Validated"
+ ],
+ "displayName": "Air Pressure",
+ "name": "airpressure",
+ "schema": "double",
+ "unit": "millibar",
+ "minimum": -1,
+ "maximum": 1000000000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..717f7fa
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/airpressure",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "airpressure": 1013.1
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..12eb7a9
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AirPressureSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"airpressure\"}",
+ "description": "Air pressure adapter",
+ "payload-mapping": {
+ "$.airpressure": "$.payload.airpressure"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "airpressure": 1023.1
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AvgAirPressureInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AvgAirPressureInstance.sh
new file mode 100644
index 0000000..4a74057
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/AirPressureEntities/AvgAirPressureInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=AverageAirPressure
+export INSTANCE_KEY=AvgArPrssr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestConfigDataIGNORE.sh b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestConfigDataIGNORE.sh
new file mode 100755
index 0000000..a3e890f
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestConfigDataIGNORE.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HALuxTestModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HALuxTestAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=luxtest
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"lux\\\": 131.1, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorDTML.json b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorDTML.json
new file mode 100644
index 0000000..8259c2f
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:luxtest;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Illuminance",
+ "Validated"
+ ],
+ "displayName": "LuxTest",
+ "name": "lux",
+ "schema": "double",
+ "unit": "lux",
+ "minimum": -1,
+ "maximum": 1000000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..beb3086
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,13 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/luxtest",
+ "reference-payload": {
+ "data": {
+ "timestamp": "2000-01-01T00:00:00.000000Z",
+ "lux": 216.1
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "${if has(\"timestamp\") then .timestamp else \"2000-01-01T00:00:00.000000Z\" end}"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..d400bd2
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/LuxTestSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,16 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"luxtest\"}",
+ "description": "Illuminance adapter",
+ "payload-mapping": {
+ "$.lux": "$.lux"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "timestamp": "2000-01-01T00:00:00.000000Z",
+ "lux": 216.1
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/UtilityLuxTestInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/UtilityLuxTestInstance.sh
new file mode 100644
index 0000000..c1be2e6
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/ContextPathLuxTestTestEntities/UtilityLuxTestInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=UtilityLux
+export INSTANCE_KEY=UtilLux
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/CreateEntity.sh b/IoTSonnenUploader/GatewayProxy/entities/CreateEntity.sh
new file mode 100755
index 0000000..2203df4
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/CreateEntity.sh
@@ -0,0 +1,92 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+# this assumes that all of the other settings have been completed, AND that we are running in the folder for the model / adaptor and entity data
+# load in the entity settings
+echo "Processing in $entitydir"
+source ./*"$ENTITY_DATA_FILE_SUFFIX"
+# make sure that we have the right variables set
+if [[ -z "$ENTITY_MODEL_NAME" ]]
+then
+ echo "variable ENTITY_MODEL_NAME is not set, cannot create entities in directory $entitydir"
+ exit -2
+else
+ echo "Model name is $ENTITY_MODEL_NAME"
+fi
+if [[ -z "$ENTITY_ADAPTER_NAME" ]]
+then
+ echo "variable ENTITY_ADAPTER_NAME is not set, cannot create entities in directory $entitydir"
+ exit -2
+else
+ echo "Adapter name is $ENTITY_ADAPTER_NAME"
+fi
+# we know that the file names must exist but let's make sure there's only one
+# the model file
+matches=(*"$ENTITY_MODEL_SUFFIX")
+if [ "${#matches[@]}" -eq 1 ] && [ -e "${matches[0]}" ]; then
+ ENTITY_MODEL_FILE_NAME="${matches[0]}"
+else
+ echo "Expected exactly one match for *$ENTITY_MODEL_SUFFIX, found ${#matches[@]}"
+ exit 1
+fi
+ENTITY_MODEL_FILE=file://$ENTITY_MODEL_FILE_NAME
+
+# the routes file
+matches=(*"$ENTITY_ROUTES_SUFFIX")
+if [ "${#matches[@]}" -eq 1 ] && [ -e "${matches[0]}" ]; then
+ ENTITY_ROUTES_FILE_NAME="${matches[0]}"
+else
+ echo "Expected exactly one match for *$ENTITY_ROUTES_FILE_NAME, found ${#matches[@]}"
+ exit 1
+fi
+ENTITY_ROUTES_FILE=file://$ENTITY_ROUTES_FILE_NAME
+
+# the envelope file
+matches=(*"$ENTITY_ENVELOPE_SUFFIX")
+if [ "${#matches[@]}" -eq 1 ] && [ -e "${matches[0]}" ]; then
+ ENTITY_ENVELOPE_FILE_NAME="${matches[0]}"
+else
+ echo "Expected exactly one match for *$ENTITY_ENVELOPE_FILE_NAME, found ${#matches[@]}"
+ exit 1
+fi
+ENTITY_ENVELOPE_FILE=file://$ENTITY_ENVELOPE_FILE_NAME
+
+echo "Working with model $ENTITY_MODEL_FILE Route $ENTITY_ROUTES_FILE and Envelope $ENTITY_ENVELOPE_FILE"
+export ENTITY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$ENTITY_MODEL_ID" ]]
+then
+ echo Creating Digital Twin Model for entity $ENTITY_MODEL_NAME
+ # Create the digtal twin model for the entiry
+ echo oci iot digital-twin-model create --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_MODEL_NAME --spec "$ENTITY_MODEL_FILE" --wait-for-state ACTIVE
+ oci iot digital-twin-model create --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_MODEL_NAME --spec "$ENTITY_MODEL_FILE" --wait-for-state ACTIVE
+ # get the id of the model you just created
+ export ENTITY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ echo Created Digital Twin entity Model $ENTITY_MODEL_NAME
+else
+ echo "Digital twin model for entity $ENTITY_MODEL_NAME already exists"
+fi
+
+export ENTITY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$ENTITY_ADAPTER_OCID" ]]
+then
+ echo Creating Digital Twin entity adapter $ENTITY_ADAPTER_NAME
+ # create the adaptor using the multiple routes file.
+ # note that as the file contents is inlined into the JSON request then the structure of the JSON files MUST be valid as JSON and also meeting the spec - if not you will get missing param errors, even though you have specified all of the actual flags correctly
+ echo oci iot digital-twin-adapter create --iot-domain-id $IOT_DOMAIN_OCID --digital-twin-model-id $ENTITY_MODEL_ID --inbound-routes "$ENTITY_ROUTES_FILE" --display-name $ENTITY_ADAPTER_NAME --inbound-envelope "$ENTITY_ENVELOPE_FILE" --wait-for-state ACTIVE
+ oci iot digital-twin-adapter create --iot-domain-id $IOT_DOMAIN_OCID --digital-twin-model-id $ENTITY_MODEL_ID --inbound-routes "$ENTITY_ROUTES_FILE" --display-name $ENTITY_ADAPTER_NAME --inbound-envelope "$ENTITY_ENVELOPE_FILE" --wait-for-state ACTIVE
+ # get the OCID of the new adaptor
+ export ENTITY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ echo Created entity adapter $ENTITY_ADAPTER_NAME
+else
+ echo "Entity adapter $ENTITY_ADAPTER_NAME already exists"
+fi
+# reset the entiry info file
+echo "Entity test commands" > $ENTITY_TEST_DATA_FILE
+echo "Looking for instance data files"
+# lets try and create the actual instances
+for instancefile in *"$ENTITY_INSTANCE_SUFFIX"; do
+ [ -f "$instancefile" ] || continue
+ echo "Instance file $instancefile"
+ ../CreateInstance.sh $instancefile
+done
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/CreateInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/CreateInstance.sh
new file mode 100755
index 0000000..6432dd9
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/CreateInstance.sh
@@ -0,0 +1,81 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+
+if [ $# -eq 1 ]
+then
+ INSTANCE_FILE=$1
+else
+ echo "CreateInstance.sh only one instance file argument is supported and it must be present"
+fi
+if [ ! -f "$INSTANCE_FILE" ]; then
+ echo "Bad argument Instance file $INSTANCE_FILE in entity directory $entitydir is missing" >&2
+ exit 1
+fi
+# this assumes that all of the other settings have been completed, AND that we are running in the folder for the model / adaptor and entity data
+# load in the entity settings
+echo "Processing instance file $INSTANCE_FILE in entiry directory $entitydir"
+source ./$INSTANCE_FILE
+# make sure that we have the right variables set
+if [[ -z "$INSTANCE_DISPLAY_NAME" ]]
+then
+ echo "variable INSTANCE_DISPLAY_NAME is not set, cannot create instance in directory $entitydir"
+ exit -2
+else
+ echo "Instance name is $INSTANCE_DISPLAY_NAME"
+fi
+if [[ -z "$INSTANCE_KEY" ]]
+then
+ echo "variable INSTANCE_KEY is not set, cannot create entities in directory $entitydir"
+ exit -2
+else
+ echo "Instance key (provisional) is $INSTANCE_KEY"
+fi
+
+export INSTANCE_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $INSTANCE_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$INSTANCE_OCID" ]]
+then
+ echo "No existing instance names $INSTANCE_DISPLAY_NAME found, will try to create it connected to gateway named $GATEWAY_DISPLAY_NAME"
+ echo oci iot digital-twin-instance create --iot-domain-id $IOT_DOMAIN_OCID --connectivity-type INDIRECT --display-name $INSTANCE_DISPLAY_NAME --gateways "[\"$GATEWAY_OCID\"]" --digital-twin-adapter-id $ENTITY_ADAPTER_OCID --external-key $INSTANCE_KEY --wait-for-state ACTIVE
+ oci iot digital-twin-instance create --iot-domain-id $IOT_DOMAIN_OCID --connectivity-type INDIRECT --display-name $INSTANCE_DISPLAY_NAME --gateways "[\"$GATEWAY_OCID\"]" --digital-twin-adapter-id $ENTITY_ADAPTER_OCID --external-key $INSTANCE_KEY --wait-for-state ACTIVE
+ export INSTANCE_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $INSTANCE_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+ export INSTANCE_KEY=`oci iot digital-twin-instance get --digital-twin-instance-id $INSTANCE_OCID | jq -r '.data."external-key"'`
+ echo "Create entity instance $INSTANCE_DISPLAY_NAME using key $INSTANCE_KEY"
+else
+ export INSTANCE_KEY=`oci iot digital-twin-instance get --digital-twin-instance-id $INSTANCE_OCID | jq -r '.data."external-key"'`
+ echo "Found existing instance for $INSTANCE_DISPLAY_NAME with key $INSTANCE_KEY"
+fi
+
+ENDPOINT=$ENDPOINT_MAIN/$ENTITY_ENDPOINT_NAME
+echo
+echo "To send the instance test data using https for device $INSTANCE_DISPLAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+#echo 'export CURRENT_TS=`date +%s%N | cut -b1-13`'
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$ENDPOINT\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": $ENTITY_SAMPLE_DATA, \\\"devicekey\\\":\\\"$INSTANCE_KEY\\\"}\"
+echo
+echo "To send instance test data using mqttx for device $INSTANCE_DISPLAY_NAME"
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z'
+#echo 'export CURRENT_TS=`date +%s%N | cut -b1-13`'
+echo mqttx pub -t $ENDPOINT -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": $ENTITY_SAMPLE_DATA, \\\"devicekey\\\":\\\"$INSTANCE_KEY\\\"}\"
+echo
+echo "To get the most recent normalized data for instance $INSTANCE_DISPLAY_NAME"
+echo "oci iot digital-twin-instance get-content --digital-twin-instance-id $INSTANCE_OCID"
+
+# now save to the entiry file
+
+echo >> $ENTITY_TEST_DATA_FILE
+echo >> $ENTITY_TEST_DATA_FILE
+echo >> $ENTITY_TEST_DATA_FILE
+echo "Testing commands for instance $INSTANCE_DISPLAY_NAME of model $ENTITY_MODEL_NAME" >> $ENTITY_TEST_DATA_FILE
+echo >> $ENTITY_TEST_DATA_FILE
+echo "To send the instance test data using https for device $INSTANCE_DISPLAY_NAME" >> $ENTITY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $ENTITY_TEST_DATA_FILE
+echo curl -u \"$GATEWAY_CREDENTIALS\" \"https://$IOT_DOMAIN_HOST/$ENDPOINT\" -H \'Content-Type: application/json\' -d \"{\\\"payload\\\": $ENTITY_SAMPLE_DATA, \\\"devicekey\\\":\\\"$INSTANCE_KEY\\\"}\" >> $ENTITY_TEST_DATA_FILE
+echo >> $ENTITY_TEST_DATA_FILE
+echo "To send instance test data using mqttx for device $INSTANCE_DISPLAY_NAME" >> $ENTITY_TEST_DATA_FILE
+echo 'export CURRENT_TS=`date -u +"%Y-%m-%dT%H:%M:%S"`.`date +%N | cut -b 1-6`Z' >> $ENTITY_TEST_DATA_FILE
+echo mqttx pub -t $ENDPOINT -ct application/json -u $GATEWAY_EXTERNAL_KEY -P $GATEWAY_INSTANCE_SECRET -h $IOT_DOMAIN_HOST -p 8883 -m \"{\\\"payload\\\": $ENTITY_SAMPLE_DATA, \\\"devicekey\\\":\\\"$INSTANCE_KEY\\\"}\" >> $ENTITY_TEST_DATA_FILE
+echo >> $ENTITY_TEST_DATA_FILE
+echo "To get the most recent normalized data for instance $INSTANCE_DISPLAY_NAME" >> $ENTITY_TEST_DATA_FILE
+echo "oci iot digital-twin-instance get-content --digital-twin-instance-id $INSTANCE_OCID" >> $ENTITY_TEST_DATA_FILE
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsConfigData.sh
new file mode 100755
index 0000000..bfe87e6
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HADailyGridEnergyFlowsModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HADailyGridEnergyFlowsAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=dailygridenergyflows
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"importkwh\\\": 1.15, \\\"exportkwh\\\": 5.5,\\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsDTML.json b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsDTML.json
new file mode 100644
index 0000000..1356123
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsDTML.json
@@ -0,0 +1,34 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:dailygridenergyflows;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Energy"
+ ],
+ "displayName": "Imported Energy",
+ "name": "importkwh",
+ "schema": "double",
+ "unit": "kilowattHour"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Energy"
+ ],
+ "displayName": "Exported Energy",
+ "name": "exportkwh",
+ "schema": "double",
+ "unit": "kilowattHour"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..f6e3a40
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,17 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/dailygridenergyflows",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "importkwh": 2.312,
+ "exportkwh": 1.2
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..697fc94
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridEnergyFlowsFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,21 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"dailygridenergyflows\"}",
+ "description": "Grid Energy Flows adapter",
+ "payload-mapping": {
+ "$.importkwh": "$.payload.importkwh",
+ "$.exportkwh": "$.payload.exportkwh"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "importkwh": 2.312,
+ "exportkwh": 1.2
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridFlowsInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridFlowsInstance.sh
new file mode 100644
index 0000000..5ab638c
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DailyGridEnergyFlowsEntities/DailyGridFlowsInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=DailyGridFlows
+export INSTANCE_KEY=DlyGrdFlws
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DeleteEntity.sh b/IoTSonnenUploader/GatewayProxy/entities/DeleteEntity.sh
new file mode 100755
index 0000000..8b2f25d
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DeleteEntity.sh
@@ -0,0 +1,48 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+# this assumes that all of the other settings have been completed, AND that we are running in the folder for the model / adaptor and entity data
+# load in the entity settings
+echo "Processing in $entitydir"
+source ./*"$ENTITY_DATA_FILE_SUFFIX"
+# lets try and delete the actual instances
+for instancefile in *"$ENTITY_INSTANCE_SUFFIX"; do
+ [ -f "$instancefile" ] || continue
+ echo "Instance file $instancefile"
+ ../DeleteInstance.sh $instancefile
+done
+
+# make sure that we have the right variables set
+if [[ -z "$ENTITY_MODEL_NAME" ]]
+then
+ echo "variable ENTITY_MODEL_NAME is not set, cannot delete entities in directory $entitydir"
+ exit -2
+fi
+if [[ -z "$ENTITY_ADAPTER_NAME" ]]
+then
+ echo "variable ENTITY_ADAPTER_NAME is not set, cannot delete entities in directory $entitydir"
+ exit -2
+fi
+export ENTITY_ADAPTER_OCID=`oci iot digital-twin-adapter list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_ADAPTER_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -n "$ENTITY_ADAPTER_OCID" ]]
+then
+ echo "Located entity adapter $ENTITY_ADAPTER_NAME, deleting it"
+ oci iot digital-twin-adapter delete --digital-twin-adapter-id $ENTITY_ADAPTER_OCID --force --wait-for-state DELETED
+ echo "Deleted entity adapter $ENTITY_ADAPTER_NAME"
+else
+ echo "Can't locate entity adapter $ENTITY_ADAPTER_NAME"
+fi
+
+export ENTITY_MODEL_ID=`oci iot digital-twin-model list --iot-domain-id $IOT_DOMAIN_OCID --display-name $ENTITY_MODEL_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -n "$ENTITY_MODEL_ID" ]]
+then
+ echo "Located digital twin entity model $ENTITY_MODEL_NAME, deleting it"
+ oci iot digital-twin-model delete --digital-twin-model-id $ENTITY_MODEL_ID --force --wait-for-state DELETED
+ echo "Deleted digital twin entity model $ENTITY_MODEL_NAME"
+else
+ echo "Can't locate digital twin entity model $ENTITY_MODEL_NAME"
+fi
+# make sure its there, then delete it
+touch $ENTITY_TEST_DATA_FILE
+rm $ENTITY_TEST_DATA_FILE
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DeleteInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DeleteInstance.sh
new file mode 100755
index 0000000..02c10eb
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DeleteInstance.sh
@@ -0,0 +1,37 @@
+#!/bin/bash -f
+# make sure wildcard expansion is enabled
+set +f
+shopt -s nullglob
+
+if [ $# -eq 1 ]
+then
+ INSTANCE_FILE=$1
+else
+ echo "DeleteInstance.sh only one instance file argument is supported and it must be present"
+fi
+if [ ! -f "$INSTANCE_FILE" ]; then
+ echo "Bad argument Instance file $INSTANCE_FILE in entity directory $entitydir is missing" >&2
+ exit 1
+fi
+# this assumes that all of the other settings have been completed, AND that we are running in the folder for the model / adaptor and entity data
+# load in the entity settings
+echo "Processing instance file $INSTANCE_FILE in entiry directory $entitydir"
+source ./$INSTANCE_FILE
+# make sure that we have the right variables set
+if [[ -z "$INSTANCE_DISPLAY_NAME" ]]
+then
+ echo "variable INSTANCE_DISPLAY_NAME is not set, cannot create instance in directory $entitydir"
+ exit -2
+else
+ echo "Instance name is $INSTANCE_DISPLAY_NAME"
+fi
+
+export INSTANCE_OCID=`oci iot digital-twin-instance list --iot-domain-id $IOT_DOMAIN_OCID --display-name $INSTANCE_DISPLAY_NAME | jq -r 'first(.data.items[] | select(."lifecycle-state" == "ACTIVE") | ."id") // empty'`
+if [[ -z "$INSTANCE_OCID" ]]
+then
+ echo "No existing instance names $INSTANCE_DISPLAY_NAME found"
+else
+ echo "Found existing entity instance for $INSTANCE_DISPLAY_NAME attempting to delete it"
+ oci iot digital-twin-instance delete --digital-twin-instance-id $INSTANCE_OCID --force --wait-for-state DELETED
+ echo "Deleted entity instance $INSTANCE_DISPLAY_NAME"
+fi
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorConfigData.sh
new file mode 100755
index 0000000..2e0e875
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HADoor
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HADoorAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=door
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"dooropen\\\": true, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorDTML.json b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorDTML.json
new file mode 100644
index 0000000..5c64ef5
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorDTML.json
@@ -0,0 +1,21 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:door;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "Door Open",
+ "name": "dooropen",
+ "schema": "boolean"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyEnvelope.json
new file mode 100644
index 0000000..83a64fd
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/door",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "dooropen": true
+ },
+ "devicekey": "abc"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyRoutes.json
new file mode 100644
index 0000000..39b1c06
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/DoorProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"door\"}",
+ "description": "Door status adapter",
+ "payload-mapping": {
+ "$.dooropen": "$.payload.dooropen"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "dooropen": true
+ },
+ "devicekey": "abc"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/FrontDoorInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/FrontDoorInstance.sh
new file mode 100644
index 0000000..30520d8
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/FrontDoorInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=FrontDoor
+export INSTANCE_KEY=MyggbettFrntDr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GarageDoorInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GarageDoorInstance.sh
new file mode 100644
index 0000000..78cd661
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GarageDoorInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=GarageDoor
+export INSTANCE_KEY=MyggbettGrgDr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GardenDoorInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GardenDoorInstance.sh
new file mode 100644
index 0000000..8c62574
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/GardenDoorInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=GardenDoor
+export INSTANCE_KEY=MyggbettGrdnDr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/KitchenDoorInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/KitchenDoorInstance.sh
new file mode 100644
index 0000000..5dc3d48
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/DoorEntities/KitchenDoorInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=KitchenDoor
+export INSTANCE_KEY=MyggbettKtchnDr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/DailyCarChargeInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/DailyCarChargeInstance.sh
new file mode 100644
index 0000000..3f17655
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/DailyCarChargeInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=CarChargeToday
+export INSTANCE_KEY=CrChrgTdy
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourConfigData.sh
new file mode 100755
index 0000000..8654d5a
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HAEnergyKWhModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAEnergyKWhAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=kilowatthours
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"kilowatthours\\\": 2.15, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourDTML.json b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourDTML.json
new file mode 100644
index 0000000..0dac24e
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourDTML.json
@@ -0,0 +1,23 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:kilowatthours;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Energy"
+ ],
+ "displayName": "KWattHours",
+ "name": "kilowatthours",
+ "schema": "double",
+ "unit": "kilowattHour"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..08d4ec9
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/kilowatthours",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "kilowatthours": 2.312
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..8875a59
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/EnergyKiloWattHourEntities/EnergyKiloWattHourSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"kilowatthours\"}",
+ "description": "Energy KWh adapter",
+ "payload-mapping": {
+ "$.kilowatthours": "$.payload.kilowatthours"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "kilowatthours": 13.2
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeConfigData.sh
new file mode 100755
index 0000000..d5ec6ad
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HAHomeBatteryChargePercentModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAHomeBatteryChargePercentAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=energy
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"userchargepercent\\\": 13.1, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeInstance.sh
new file mode 100644
index 0000000..e6ccf2a
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=SonnenUserCharge
+export INSTANCE_KEY=SnnBatUsrChrg
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorDTML.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorDTML.json
new file mode 100644
index 0000000..ae577e0
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorDTML.json
@@ -0,0 +1,24 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:homebatterycharge;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Validated"
+ ],
+ "displayName": "User Charge Level Percentage",
+ "name": "userchargepercent",
+ "schema": "double",
+ "minimum": 0,
+ "maximum": 100
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..1e52370
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/energy",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "userchargepercent": 26.1
+ },
+ "devicekey": "abc"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..aeeb5db
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryChargeEntities/HomeBatteryChargeSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"energy\"}",
+ "description": "Battery Charge adapter",
+ "payload-mapping": {
+ "$.userchargepercent": "$.payload.userchargepercent"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "userchargepercent": 21.1
+ },
+ "devicekey": "abc"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveConfigData.sh
new file mode 100755
index 0000000..7f4b433
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HAHomeBatteryManualReserveSettings
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAHomeBatteryManualReserveSettingsAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=homebatterymanualreservesettings
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"manualreserveenabled\\\": false, \\\"repeatmanualreserveenabled\\\": true, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsDTML.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsDTML.json
new file mode 100644
index 0000000..7904929
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsDTML.json
@@ -0,0 +1,30 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:homebatterymanualreservesettings;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "Battery Manual Reserve Enabled",
+ "name": "manualreserveenabled",
+ "schema": "boolean"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Historized"
+ ],
+ "displayName": "Battery Manual Reserve Repeat Enabled",
+ "name": "repeatmanualreserveenabled",
+ "schema": "boolean"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..4f252b3
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,17 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/homebatterymanualreservesettings",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "repeatmanualreserveenabled": true,
+ "manualreserveenabled": true
+ },
+ "devicekey": "abc"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..44c3f3a
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/HomeBatteryManualReserveSettingsFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,21 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"homebatterymanualreservesettings\"}",
+ "description": "Battery Manual Reserve Controls adapter",
+ "payload-mapping": {
+ "$.repeatmanualreserveenabled": "$.payload.repeatmanualreserveenabled",
+ "$.manualreserveenabled": "$.payload.manualreserveenabled"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "repeatmanualreserveenabled": true,
+ "manualreserveenabled": true
+ },
+ "devicekey": "abc"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/SonnenManualReserveInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/SonnenManualReserveInstance.sh
new file mode 100644
index 0000000..62f10a8
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/HomeBatteryManualReserveEntities/SonnenManualReserveInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=SonnenManualReserveSettings
+export INSTANCE_KEY=SnnManRsrvSttngs
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceConfigData.sh
new file mode 100755
index 0000000..4ed9ade
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HAIlluminanceModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAIlluminanceAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=illuminance
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"lux\\\": 131.1, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorDTML.json b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorDTML.json
new file mode 100644
index 0000000..8e3feb6
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:luminance;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Illuminance",
+ "Validated"
+ ],
+ "displayName": "Illuminance",
+ "name": "lux",
+ "schema": "double",
+ "unit": "lux",
+ "minimum": -1,
+ "maximum": 1000000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..d827636
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/illuminance",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "lux": 216.1
+ },
+ "devicekey": "abc"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..bb1bed7
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/IlluminanceSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"illuminance\"}",
+ "description": "Illuminance adapter",
+ "payload-mapping": {
+ "$.lux": "$.payload.lux"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "lux": 216.1
+ },
+ "devicekey": "abc"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/UtilityIlluminanceInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/UtilityIlluminanceInstance.sh
new file mode 100644
index 0000000..f03d02e
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/IlluminanceEntities/UtilityIlluminanceInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=UtilityLuminance
+export INSTANCE_KEY=UtilIlluminance
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/CarChargingPowerInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/CarChargingPowerInstance.sh
new file mode 100644
index 0000000..ce5206e
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/CarChargingPowerInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=CarChargerPower
+export INSTANCE_KEY=CrChrgrPwr
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/FilteredGridConsumptionInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/FilteredGridConsumptionInstance.sh
new file mode 100644
index 0000000..f60dc73
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/FilteredGridConsumptionInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=FilteredGridConsumption
+export INSTANCE_KEY=FiltGridCons
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsConfigData.sh
new file mode 100755
index 0000000..96f6d8e
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HAPowerModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HAPowerAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=power
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"watts\\\": 2315, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsDTML.json b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsDTML.json
new file mode 100644
index 0000000..3969c4b
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:power;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Power",
+ "Validated"
+ ],
+ "displayName": "Watts",
+ "name": "watts",
+ "schema": "double",
+ "unit": "watt",
+ "minimum": 0,
+ "maximum": 1000000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..1430d53
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/power",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "watts": 2327
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..1e36325
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/PowerWattsSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"power\"}",
+ "description": "Power adapter",
+ "payload-mapping": {
+ "$.watts": "$.payload.watts"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "watts": 3162
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/SolarGenerationCombinedInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/SolarGenerationCombinedInstance.sh
new file mode 100644
index 0000000..bd19931
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/PowerWattsEntities/SolarGenerationCombinedInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=SolarGenerationCombined
+export INSTANCE_KEY=SolGenTotal
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgIndoorRelativeHumidityInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgIndoorRelativeHumidityInstance.sh
new file mode 100644
index 0000000..a085675
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgIndoorRelativeHumidityInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=AverageIndoorRelativeHumidity
+export INSTANCE_KEY=AvgIndrRltvHmdty
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgOutdoorRelativeHumidityInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgOutdoorRelativeHumidityInstance.sh
new file mode 100644
index 0000000..f1251de
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/AvgOutdoorRelativeHumidityInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=AverageOutdoorRelativeHumidity
+export INSTANCE_KEY=AvgOtdrRltvHmdty
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityConfigData.sh
new file mode 100755
index 0000000..0ae4f87
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HARelativeHumidityModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HARelativeHumidityAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=relativehumidity
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"relativehumidity\\\": 53.4, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityDTML.json b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityDTML.json
new file mode 100644
index 0000000..d7068a1
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumidityDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:relativehumidity;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "RelativeHumidity",
+ "Validated"
+ ],
+ "displayName": "RelativeHumidity",
+ "name": "relativehumidity",
+ "schema": "double",
+ "unit": "percent",
+ "minimum": -1,
+ "maximum": 100
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..1fdc74a
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/relativehumidity",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "relativehumidity": 53.4
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..f51658a
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/RelativeHumidityEntities/RelativeHumiditySensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"relativehumidity\"}",
+ "description": "Humidity adapter",
+ "payload-mapping": {
+ "$.relativehumidity": "$.payload.relativehumidity"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "relativehumidity": 65.2
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/.gitignore b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/.gitignore
new file mode 100644
index 0000000..a914c69
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/.gitignore
@@ -0,0 +1 @@
+/EntityTestData.txt
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgIndoorTemperatureInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgIndoorTemperatureInstance.sh
new file mode 100644
index 0000000..023ab4f
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgIndoorTemperatureInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=AverageIndoorTemperature
+export INSTANCE_KEY=AvgIndrTmp
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgOutdoorTemperatureInstance.sh b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgOutdoorTemperatureInstance.sh
new file mode 100644
index 0000000..e3a621f
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/AvgOutdoorTemperatureInstance.sh
@@ -0,0 +1,2 @@
+export INSTANCE_DISPLAY_NAME=AverageOutdoorTemperature
+export INSTANCE_KEY=AvgOtdrTmp
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureConfigData.sh b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureConfigData.sh
new file mode 100755
index 0000000..396da3e
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureConfigData.sh
@@ -0,0 +1,16 @@
+# Model data for Illuminance
+# display name of the model (its file name is worked out based on the suffix
+# DTMI.json)
+export ENTITY_MODEL_NAME=HATempModel
+# display name of the adapter (its envelope and route file names are worked
+# out based on the suffixs Envelope.json and Rutes.json respetivly)
+export ENTITY_ADAPTER_NAME=HATempAdapter
+# the last part of the endpoint, used for the sample data commands, if the
+# routes file has a condition based on the endpoint then this must match
+export ENTITY_ENDPOINT_NAME=temperature
+# sample payload for the sample data commands, be very carefull about
+# escaping, and for the timestamp field use the variable $CURRENT_TS
+# as the sample code will generate a line to set it. The sample generator
+# will also produce the wrapper json which specifies the devicekey
+# for each instance
+export ENTITY_SAMPLE_DATA="{\\\"temperature\\\": 22.1, \\\"timestamp\\\": \\\"\$CURRENT_TS\\\"}"
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureDTML.json b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureDTML.json
new file mode 100644
index 0000000..4336d42
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureDTML.json
@@ -0,0 +1,26 @@
+{
+ "@context": [
+ "dtmi:dtdl:context;3",
+ "dtmi:dtdl:extension:historization;1",
+ "dtmi:dtdl:extension:quantitativeTypes;1",
+ "dtmi:com:oracle:dtdl:extension:validation;1"
+ ],
+ "@id": "dtmi:com:oracle:demo:timg:iot:homeassistant:temperature;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Historized",
+ "Temperature",
+ "Validated"
+ ],
+ "displayName": "Temperature",
+ "name": "temperature",
+ "schema": "double",
+ "unit": "degreeCelsius",
+ "minimum": -274,
+ "maximum": 1000000000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyEnvelope.json b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyEnvelope.json
new file mode 100644
index 0000000..4a532f1
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyEnvelope.json
@@ -0,0 +1,16 @@
+{
+ "reference-endpoint": "house/homeassistant/entities/temperature",
+ "reference-payload": {
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "temperature": 22.1
+ },
+ "devicekey": "def"
+ },
+ "data-format": "JSON"
+ },
+ "envelope-mapping": {
+ "timeObserved": "$.payload.timestamp"
+ }
+}
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyRoutes.json b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyRoutes.json
new file mode 100644
index 0000000..86a7ed1
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/TemperatureEntities/TemperatureSensorFromHomeAssistantProxyRoutes.json
@@ -0,0 +1,19 @@
+[
+ {
+ "condition": "${endpoint(1) == \"house\" and endpoint(2) == \"homeassistant\" and endpoint(3) == \"entities\" and endpoint(4) == \"temperature\"}",
+ "description": "Temperature adapter",
+ "payload-mapping": {
+ "$.temperature": "$.payload.temperature"
+ },
+ "reference-payload": {
+ "dataFormat": "JSON",
+ "data": {
+ "payload": {
+ "timestamp": "2026-07-10T16:09:18.934123Z",
+ "temperature": 23.1
+ },
+ "devicekey": "def"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/entities/entity_names.sh b/IoTSonnenUploader/GatewayProxy/entities/entity_names.sh
new file mode 100755
index 0000000..07a3837
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/entities/entity_names.sh
@@ -0,0 +1,9 @@
+# these are the names that are used to scan and load things
+export ENTITY_TEST_DATA_FILE=./EntityTestData.txt
+export ENDPOINT_MAIN=house/homeassistant/entities
+export ENTITY_DIRECTORY_SUFFIX=Entities
+export ENTITY_MODEL_SUFFIX=DTML.json
+export ENTITY_ROUTES_SUFFIX=Routes.json
+export ENTITY_ENVELOPE_SUFFIX=Envelope.json
+export ENTITY_DATA_FILE_SUFFIX=ConfigData.sh
+export ENTITY_INSTANCE_SUFFIX=Instance.sh
\ No newline at end of file
diff --git a/IoTSonnenUploader/GatewayProxy/gateway_names.sh b/IoTSonnenUploader/GatewayProxy/gateway_names.sh
new file mode 100755
index 0000000..95228b9
--- /dev/null
+++ b/IoTSonnenUploader/GatewayProxy/gateway_names.sh
@@ -0,0 +1,17 @@
+# these define the various names for the gateway objects
+export GATEWAY_TEST_DATA_FILE=./GatewayTestData.txt
+export GATEWAY_MODEL_NAME=hagateway
+export GATEWAY_MODEL_FILE=file://GatewayDTML.json
+export GATEWAY_ADAPTOR_ROUTE_MAPPINGS_FILE=file://GatewayProxyRoutes.json
+export GATEWAY_ADAPTOR_ENVELOPE_MAPPINGS_FILE=file://GatewayProxyEnvelope.json
+export GATEWAY_ADAPTER_NAME=gateway-adapter
+# do not change these endpoint paths without also altering the Java code and the DTML route files in the adapter
+export GATEWAY_ENDPOINT_PREFIX=house/homeassistant
+export GATEWAY_ENDPOINT_GATEWAY_PATH=$GATEWAY_ENDPOINT_PREFIX/gateway
+export GATEWAY_ENDPOINT_GATEWAY_STATS_PATH=$GATEWAY_ENDPOINT_GATEWAY_PATH/stats
+export GATEWAY_ENDPOINT_GATEWAY_CONFIG_PATH=$GATEWAY_ENDPOINT_GATEWAY_PATH/config
+export GATEWAY_ENDPOINT_ENTITIES_PATH=$GATEWAY_ENDPOINT_PREFIX/entities
+export GATEWAY_NAME=hagateway
+export GATEWAY_DISPLAY_NAME=HomeAssistantGateway
+
+export GATEWAY_ENTITIES_DIRECTORY=entities
\ No newline at end of file
diff --git a/IoTSonnenUploader/OCISetup/ConfigureIOTForDirectDBAccess.sh b/IoTSonnenUploader/OCISetup/ConfigureIOTForDirectDBAccess.sh
index 2b56c1e..3743be1 100755
--- a/IoTSonnenUploader/OCISetup/ConfigureIOTForDirectDBAccess.sh
+++ b/IoTSonnenUploader/OCISetup/ConfigureIOTForDirectDBAccess.sh
@@ -81,5 +81,5 @@ echo "Config settings for the IoTDBJDBC"
echo iotdatacache.schemaname="$IOT_DOMAIN_SHORT_ID"__iot
echo iotdatacache.ociregion=$OCI_CONFIG_REGION
echo iotdatacache.connectionname=$DB_CONNECTION_NAME
-echo oci.dbtoken.compartment=$DB_COMPARTMENT_NAME
-echo oci.auth.type=InstancePrinciple
\ No newline at end of file
+echo iotdatacache.oci.dbtoken.compartment=$DB_COMPARTMENT_NAME
+echo iotdatacache.oci.auth.type=InstancePrinciple
\ No newline at end of file
diff --git a/IoTSonnenUploader/OCISetup/CreateBastionSession.sh b/IoTSonnenUploader/OCISetup/CreateBastionSession.sh
index 16ea640..542f1ba 100755
--- a/IoTSonnenUploader/OCISetup/CreateBastionSession.sh
+++ b/IoTSonnenUploader/OCISetup/CreateBastionSession.sh
@@ -7,17 +7,20 @@ echo "Using OCI config profile $OCI_CLI_PROFILE"
IOT_COMPARTMENT_OCID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
-BASTION_OCID=`oci bastion bastion list --compartment-id "$IOT_COMPARTMENT_OCID" --name "$BASTION_NAME" --all --query "data[0].id" --raw-output`
-
-
+BASTION_OCID=`oci bastion bastion list --compartment-id "$IOT_COMPARTMENT_OCID" --name "$BASTION_NAME" --all | jq -r '.data[]| select (."lifecycle-state" == "ACTIVE") | ."id"'`
+if [ -z "$BASTION_OCID" ] ; then
+ echo "Error: Could not find Bastion $BASTION_NAME."
+ exit 1
+fi
echo "Bastion $BASTION_NAME in compartment $IOT_COMPARTMENT_OCID has $BASTION_OCID ocid"
-# Retrieve Target Instance OCID by Name
-VM_OCID=`oci compute instance list --compartment-id "$IOT_COMPARTMENT_OCID" --display-name "$VM_NAME" --query "data[0].id" --raw-output`
+# Retrieve Target Instance OCID by Name select(.lifecycleState | IN("STOPPED", "RUNNING", "STARTING", "STOPPING"))
+#VM_OCID=`oci compute instance list --compartment-id "$IOT_COMPARTMENT_OCID" --display-name "$VM_NAME" --query "data[0].id" --raw-output`
+VM_OCID=`oci compute instance list --compartment-id "$IOT_COMPARTMENT_OCID" --display-name "$VM_NAME" --all | jq -r '.data[]| select (."lifecycle-state" | IN("STOPPED", "RUNNING", "STARTING", "STOPPING")) | ."id"'`
echo "Compute instance $VM_NAME in compartment $IOT_COMPARTMENT_OCID has $VM_OCID ocid"
-if [ -z "$BASTION_OCID" ] || [ -z "$VM_OCID" ]; then
- echo "Error: Could not find Bastion or Target Instance."
+if [ -z "$VM_OCID" ]; then
+ echo "Error: Could not find Target Instance."
exit 1
fi
diff --git a/IoTSonnenUploader/OCISetup/DBClientVMCloudInit.sh b/IoTSonnenUploader/OCISetup/DBClientVMCloudInit.sh
new file mode 100644
index 0000000..77322e5
--- /dev/null
+++ b/IoTSonnenUploader/OCISetup/DBClientVMCloudInit.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+set -euxo pipefail
+
+LOG_FILE=/var/log/db-client-cloud-init.log
+exec > >(tee -a "$LOG_FILE") 2>&1
+
+echo "Starting DB client VM bootstrap at $(date -Is)"
+
+dnf clean all
+dnf makecache -y
+
+mapfile -t JAVA_PACKAGES < <(rpm -qa | grep -Ei '(^java-|^jdk-|^jre-|openjdk)' || true)
+if (( ${#JAVA_PACKAGES[@]} > 0 ))
+then
+ dnf remove -y "${JAVA_PACKAGES[@]}"
+fi
+
+dnf install -y java-25-openjdk-devel git maven curl unzip
+
+JAVA_HOME=$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")
+SQLCL_HOME=/opt/sqlcl
+
+SQLCL_ZIP=/tmp/sqlcl-latest.zip
+curl --fail --location --show-error --output "$SQLCL_ZIP" https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip
+rm -rf "$SQLCL_HOME" /opt/sqlcl-*
+unzip -q "$SQLCL_ZIP" -d /opt
+ln -sfn "$SQLCL_HOME/bin/sql" /usr/local/bin/sql
+ln -sfn "$SQLCL_HOME/bin/sql" /usr/local/bin/sqlcl
+chmod +x "$SQLCL_HOME/bin/sql"
+
+SYSTEM_PATH="$JAVA_HOME/bin:$SQLCL_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+
+rm -f /etc/profile.d/java.sh /etc/profile.d/sqlcl.sh
+cat >/etc/profile.d/db-client-tools.sh </etc/environment < 0 ))
+ do
+ if ! state=$(oci compute instance get --instance-id "$instance_id" --query 'data."lifecycle-state"' --raw-output 2>/dev/null)
+ then
+ echo "✅ Instance no longer returned by OCI: $instance_id"
+ return 0
+ fi
+
+ if [[ "$state" == "TERMINATED" ]]
+ then
+ echo "✅ Instance terminated: $instance_id"
+ return 0
+ fi
+
+ echo "⏳ Waiting for instance $instance_id to terminate (state: $state)..."
+ sleep 10
+ attempts=$((attempts - 1))
+ done
+
+ echo "⚠️ Timed out waiting for instance $instance_id to terminate"
+ return 1
+}
+
+delete_boot_volume_if_present() {
+ local boot_volume_id="$1"
+ local state
+
+ if ! state=$(oci bv boot-volume get --boot-volume-id "$boot_volume_id" --query 'data."lifecycle-state"' --raw-output 2>/dev/null)
+ then
+ echo "✅ Boot volume already removed: $boot_volume_id"
+ return 0
+ fi
+
+ if [[ "$state" == "TERMINATED" ]]
+ then
+ echo "✅ Boot volume already terminated: $boot_volume_id"
+ return 0
+ fi
+
+ echo "🗑️ Deleting remaining boot volume: $boot_volume_id"
+ oci bv boot-volume delete --boot-volume-id "$boot_volume_id" --force --wait-for-state TERMINATED
+}
+
+report_network_security_groups() {
+ local instance_id="$1"
+ local nsg_ids
+
+ nsg_ids=$(oci compute instance list-vnics --instance-id "$instance_id" --all | jq -r '.data[] | (."nsg-ids" // [])[]' | sort -u)
+ if [[ -z "$nsg_ids" ]]
+ then
+ echo "ℹ️ No Network Security Group attachments found on VM VNICs."
+ return 0
+ fi
+
+ echo "ℹ️ VM VNICs are attached to these Network Security Groups:"
+ echo "$nsg_ids"
+ echo "ℹ️ No NSG rule changes are required; OCI removes VNIC membership from NSGs when the VM VNICs are deleted."
+}
+
+echo "🔍 Searching for VM instances named $VM_NAME..."
+
+INSTANCE_IDS=$(oci compute instance list --compartment-id "$COMPARTMENT_ID" --display-name "$VM_NAME" --all | find_active_ids_by_display_name "$VM_NAME")
+if [[ -z "$INSTANCE_IDS" ]]
+then
+ echo "ℹ️ No active instance found with name $VM_NAME."
+ exit 0
+fi
+
+while read -r INSTANCE_ID
+do
+ if [[ -z "$INSTANCE_ID" ]]
+ then
+ continue
+ fi
+
+ echo "📍 Found instance: $INSTANCE_ID"
+ report_network_security_groups "$INSTANCE_ID"
+
+ BOOT_VOLUME_IDS=$(oci compute boot-volume-attachment list --compartment-id "$COMPARTMENT_ID" --instance-id "$INSTANCE_ID" --all | jq -r '.data[] | select((."lifecycle-state" // "") != "DETACHED") | ."boot-volume-id"')
+
+ echo "🗑️ Terminating instance and deleting boot volume: $INSTANCE_ID"
+ oci compute instance terminate --instance-id "$INSTANCE_ID" --preserve-boot-volume false --force --wait-for-state SUCCEEDED
+ wait_for_instance_terminated "$INSTANCE_ID"
+
+ while read -r BOOT_VOLUME_ID
+ do
+ if [[ -n "$BOOT_VOLUME_ID" ]]
+ then
+ delete_boot_volume_if_present "$BOOT_VOLUME_ID"
+ fi
+ done <<< "$BOOT_VOLUME_IDS"
+done <<< "$INSTANCE_IDS"
+
+echo "✨ VM cleanup complete. Network resources were left in place for SetupDBClientVCNandVM.sh to reuse."
diff --git a/IoTSonnenUploader/OCISetup/DestroyDBClientVMandVCN.sh b/IoTSonnenUploader/OCISetup/DestroyDBClientVMandVCN.sh
index 1b9b8a3..2695cd7 100755
--- a/IoTSonnenUploader/OCISetup/DestroyDBClientVMandVCN.sh
+++ b/IoTSonnenUploader/OCISetup/DestroyDBClientVMandVCN.sh
@@ -3,73 +3,206 @@
# run this in the DigitalTwin directory
source ../OCISetup/common_names.sh
echo "Using OCI config profile $OCI_CLI_PROFILE"
+
# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
COMPARTMENT_ID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
+echo "✅ Located compartment id: $COMPARTMENT_ID"
+
+find_active_ids_by_display_name() {
+ local display_name="$1"
+ jq -r --arg display_name "$display_name" '.data[] | select(."display-name" == $display_name) | select((."lifecycle-state" // "") != "TERMINATED") | .id'
+}
+
+find_available_id_by_display_name() {
+ local display_name="$1"
+ jq -r --arg display_name "$display_name" 'first(.data[] | select(."display-name" == $display_name) | select((."lifecycle-state" // "") == "AVAILABLE") | .id) // empty'
+}
+
+find_bastion_ids_by_name() {
+ local bastion_name="$1"
+ jq -r --arg bastion_name "$bastion_name" '.data[] | select(.name == $bastion_name) | select((."lifecycle-state" // "") != "DELETED") | .id'
+}
+
+wait_until_deleted() {
+ local resource_description="$1"
+ shift
+
+ local attempts=30
+ local state
+
+ while (( attempts > 0 ))
+ do
+ if ! state=$("$@" --query 'data."lifecycle-state"' --raw-output 2>/dev/null)
+ then
+ echo "✅ $resource_description deleted"
+ return 0
+ fi
+
+ if [[ "$state" == "TERMINATED" || "$state" == "DELETED" ]]
+ then
+ echo "✅ $resource_description deleted"
+ return 0
+ fi
+
+ echo "⏳ Waiting for $resource_description to delete (state: $state)..."
+ sleep 10
+ attempts=$((attempts - 1))
+ done
+
+ echo "⚠️ Timed out waiting for $resource_description to delete"
+ return 1
+}
+
+delete_bastion_by_name() {
+ local bastion_name="$1"
+ local bastion_id
+
+ oci bastion bastion list --compartment-id "$COMPARTMENT_ID" --name "$bastion_name" --all | find_bastion_ids_by_name "$bastion_name" | while read -r bastion_id
+ do
+ if [[ -n "$bastion_id" ]]
+ then
+ echo "🗑️ Deleting Bastion $bastion_name: $bastion_id"
+ oci bastion bastion delete --bastion-id "$bastion_id" --force --wait-for-state SUCCEEDED
+ wait_until_deleted "Bastion $bastion_name" oci bastion bastion get --bastion-id "$bastion_id"
+ fi
+ done
+}
+
+delete_subnet_by_name() {
+ local subnet_name="$1"
+ local subnet_id
+
+ oci network subnet list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$subnet_name" | while read -r subnet_id
+ do
+ if [[ -n "$subnet_id" ]]
+ then
+ echo "🗑️ Deleting Subnet $subnet_name: $subnet_id"
+ oci network subnet delete --subnet-id "$subnet_id" --force
+ wait_until_deleted "Subnet $subnet_name" oci network subnet get --subnet-id "$subnet_id"
+ fi
+ done
+}
+
+delete_route_table_by_name() {
+ local route_table_name="$1"
+ local route_table_id
+
+ oci network route-table list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$route_table_name" | while read -r route_table_id
+ do
+ if [[ -n "$route_table_id" ]]
+ then
+ echo "🗑️ Deleting Route Table $route_table_name: $route_table_id"
+ oci network route-table delete --rt-id "$route_table_id" --force
+ wait_until_deleted "Route Table $route_table_name" oci network route-table get --rt-id "$route_table_id"
+ fi
+ done
+}
+
+delete_security_list_by_name() {
+ local security_list_name="$1"
+ local security_list_id
+
+ oci network security-list list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$security_list_name" | while read -r security_list_id
+ do
+ if [[ -n "$security_list_id" ]]
+ then
+ echo "🗑️ Deleting Security List $security_list_name: $security_list_id"
+ oci network security-list delete --security-list-id "$security_list_id" --force
+ wait_until_deleted "Security List $security_list_name" oci network security-list get --security-list-id "$security_list_id"
+ fi
+ done
+}
+
+delete_service_gateway_by_name() {
+ local service_gateway_name="$1"
+ local service_gateway_id
+
+ oci network service-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$service_gateway_name" | while read -r service_gateway_id
+ do
+ if [[ -n "$service_gateway_id" ]]
+ then
+ echo "🗑️ Deleting Service Gateway $service_gateway_name: $service_gateway_id"
+ oci network service-gateway delete --service-gateway-id "$service_gateway_id" --force
+ wait_until_deleted "Service Gateway $service_gateway_name" oci network service-gateway get --service-gateway-id "$service_gateway_id"
+ fi
+ done
+}
+
+delete_internet_gateway_by_name() {
+ local internet_gateway_name="$1"
+ local internet_gateway_id
+
+ oci network internet-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$internet_gateway_name" | while read -r internet_gateway_id
+ do
+ if [[ -n "$internet_gateway_id" ]]
+ then
+ echo "🗑️ Deleting Internet Gateway $internet_gateway_name: $internet_gateway_id"
+ oci network internet-gateway delete --ig-id "$internet_gateway_id" --force
+ wait_until_deleted "Internet Gateway $internet_gateway_name" oci network internet-gateway get --ig-id "$internet_gateway_id"
+ fi
+ done
+}
+
+delete_nat_gateway_by_name() {
+ local nat_gateway_name="$1"
+ local nat_gateway_id
+
+ oci network nat-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_active_ids_by_display_name "$nat_gateway_name" | while read -r nat_gateway_id
+ do
+ if [[ -n "$nat_gateway_id" ]]
+ then
+ echo "🗑️ Deleting NAT Gateway $nat_gateway_name: $nat_gateway_id"
+ oci network nat-gateway delete --nat-gateway-id "$nat_gateway_id" --force
+ wait_until_deleted "NAT Gateway $nat_gateway_name" oci network nat-gateway get --nat-gateway-id "$nat_gateway_id"
+ fi
+ done
+}
echo "🔍 Searching for resources associated with $VCN_NAME..."
# --- 1. Find the Instance and Terminate it ---
-INSTANCE_ID=$(oci compute instance list --compartment-id "$COMPARTMENT_ID" --display-name "$VM_NAME" --lifecycle-state RUNNING --query "data[0].id" --raw-output)
-
-if [ -n "$INSTANCE_ID" ] && [ "$INSTANCE_ID" != "null" ]; then
- echo "🗑️ Terminating Instance and waititing for completion: $INSTANCE_ID"
- oci compute instance terminate --instance-id "$INSTANCE_ID" --preserve-boot-volume false --force --wait-for-state TERMINATED
- #echo "⏳ Waiting for instance to fully terminate..."
- #teroci compute instance wait-for-state --instance-id "$INSTANCE_ID" --status TERMINATED
-else
- echo "ℹ️ No running instance found with name $VM_NAME."
-fi
+oci compute instance list --compartment-id "$COMPARTMENT_ID" --display-name "$VM_NAME" --all | find_active_ids_by_display_name "$VM_NAME" | while read -r instance_id
+do
+ if [[ -n "$instance_id" ]]
+ then
+ echo "🗑️ Terminating Instance and waiting for completion: $instance_id"
+ oci compute instance terminate --instance-id "$instance_id" --preserve-boot-volume false --force --wait-for-state TERMINATED
+ fi
+done
# --- 2. Find the VCN ID to locate child resources ---
-VCN_ID=$(oci network vcn list --compartment-id "$COMPARTMENT_ID" --display-name "$VCN_NAME" --query "data[0].id" --raw-output)
+VCN_ID=$(oci network vcn list --compartment-id "$COMPARTMENT_ID" --all | find_available_id_by_display_name "$VCN_NAME")
-if [ -z "$VCN_ID" ] || [ "$VCN_ID" == "null" ]; then
- echo "❌ VCN $VCN_NAME not found. Nothing to delete."
- exit 0
+if [[ -z "$VCN_ID" ]]
+then
+ echo "ℹ️ VCN $VCN_NAME not found. Nothing to delete."
+ exit 0
fi
echo "📍 Found VCN: $VCN_ID. Cleaning up child resources..."
-# --- 3. Delete Subnets ---
+# --- 3. Delete the bastion before the target subnet ---
+delete_bastion_by_name "$BASTION_NAME"
-for SUBNET in $(oci network subnet list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[*].id" --raw-output); do
- echo "🗑️ Deleting Subnet: $SUBNET"
- oci network subnet delete --subnet-id "$SUBNET" --force
-done
+# --- 4. Delete Subnets before their route table and security list dependencies ---
+delete_subnet_by_name "$IOT_CLIENT_VM_SUBNET"
+delete_subnet_by_name "$IOT_APPS_SUBNET"
+delete_subnet_by_name "IoT_Subnet"
-# --- 4. Delete Route Tables (Skipping the default one) ---
-for RT in $(oci network route-table list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[?\"is-default\" == \`false\`].id" --raw-output); do
- echo "🗑️ Deleting Route Table: $RT"
- oci network route-table delete --rt-id "$RT" --force
-done
+# --- 5. Delete the route table before gateways referenced by its route rules ---
+delete_route_table_by_name "Public_RT"
-# --- 5. Delete Security Lists (Skipping the default one) ---
-for SL in $(oci network security-list list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[?\"is-default\" == \`false\`].id" --raw-output); do
- echo "🗑️ Deleting Security List: $SL"
- oci network security-list delete --security-list-id "$SL" --force
-done
+# --- 6. Delete the security list after subnets are gone ---
+delete_security_list_by_name "VCN_Wide_DB_Security"
-# --- 6. Delete Gateways ---
-# Service Gateway
-for SG in $(oci network service-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[*].id" --raw-output); do
- echo "🗑️ Deleting Service Gateway: $SG"
- oci network service-gateway delete --service-gateway-id "$SG" --force
-done
-
-# Internet Gateway
-for IG in $(oci network internet-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[*].id" --raw-output); do
- echo "🗑️ Deleting Internet Gateway: $IG"
- oci network internet-gateway delete --ig-id "$IG" --force
-done
-
-# NAT Gateway
-for NG in $(oci network nat-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --query "data[*].id" --raw-output); do
- echo "🗑️ Deleting NAT Gateway: $NG"
- oci network nat-gateway delete ----nat-gateway-id "$NG" --force
-done
+# --- 7. Delete gateways ---
+delete_service_gateway_by_name "SG"
+delete_internet_gateway_by_name "IGW"
+delete_nat_gateway_by_name "NAT_Gateway"
-# --- 7. Finally, Delete the VCN ---
+# --- 8. Finally, Delete the VCN ---
echo "🗑️ Deleting VCN: $VCN_ID"
oci network vcn delete --vcn-id "$VCN_ID" --force
+wait_until_deleted "VCN $VCN_NAME" oci network vcn get --vcn-id "$VCN_ID"
-echo "✨ Cleanup Complete!"
\ No newline at end of file
+echo "✨ Cleanup Complete!"
diff --git a/IoTSonnenUploader/OCISetup/SQLDBAccessClientSetupVM.txt b/IoTSonnenUploader/OCISetup/SQLDBAccessClientSetupVM.txt
index c392b57..ac69f62 100644
--- a/IoTSonnenUploader/OCISetup/SQLDBAccessClientSetupVM.txt
+++ b/IoTSonnenUploader/OCISetup/SQLDBAccessClientSetupVM.txt
@@ -1,13 +1,46 @@
-Setup a Linux instance VM connected to the VCN as per the IoT setup (be sure to enable the basion agent)
+#Setup a Linux instance VM connected to the VCN as per the IoT setup (be sure to enable the basion agent)
+# general update
+sudo dnf update --assumeyes
-once VM is in place and you're connected using the bastion (The CreatebastionSession.sh can set this up for you)
-install java and maven to allow for compilation as well as the sqlcl command
-we also need git to get the code itself
-# install the java version
+# install the oci command and it's dependencies
+sudo dnf -y install oraclelinux-developer-release-el9 --assumeyes
+sudo dnf install python39-oci-cli --assumeyes
+
+#once VM is in place and you're connected using the bastion (The CreatebastionSession.sh can set this up for you)
+#€install java and maven to allow for compilation as well as the sqlcl command
+#we also need git to get the code itself
+# install the desired java version
sudo dnf install java-25-openjdk-devel --assumeyes
+# try to switch to the new version
+sudo alternatives --config java
+sudo alternatives --config javac
+# For Oracle alternatives system
+# to list older versions of java if needed
+alternatives --list | grep java
+# to uninstall one (e.g. jre_17)
+sudo alternatives --remove-all jre_17
+sudo alternatives --remove-all jre_openjdk
+# for java installed using RPM
+dnf list --installed 'java-*-openjdk*' 'jdk-*'
+# then to remove
+sudo dnf remove java-17-openjdk-headless
+# make sure it's what we want
+sudo alternatives --display java
+sudo alternatives --display javac
+
+make sure that the path is setup, if it's not then you'll need to do so
+echo $JAVA_HOME
+# this shoudl work
+sudo alternatives --config java
+#(then chose the java version you want)
+# logout then back in
+java -version
+#if it's not there (or wrong) then get the best path with and add to ~/.bash_profile
+
+# IMPORTANT, you will then need to log out and back in
# this insista on installing jdk 17 with maven, so don't do that
#sudo dnf install maven --assumeyes
-# get maven
+# get maven, the dnf install method insiste oon installing jdk 17 and that's not what we want so we have to do it manually
cd /tmp
curl -LO https://dlcdn.apache.org/maven/maven-3/3.9.16/binaries/apache-maven-3.9.16-bin.tar.gz
diff --git a/IoTSonnenUploader/OCISetup/SetupDBClientVCNandVM.sh b/IoTSonnenUploader/OCISetup/SetupDBClientVCNandVM.sh
index 87ed7ad..f588822 100755
--- a/IoTSonnenUploader/OCISetup/SetupDBClientVCNandVM.sh
+++ b/IoTSonnenUploader/OCISetup/SetupDBClientVCNandVM.sh
@@ -1,71 +1,270 @@
#!/bin/bash
source ../OCISetup/common_names.sh
echo "Using OCI config profile $OCI_CLI_PROFILE"
+
# --- Configuration ---
# the get_oci_compartment_ocid.sh script is in the OCI setup folder, but I use it so much it's also in my bin directory
COMPARTMENT_ID=`get_oci_compartment_ocid.sh $COMPARTMENT_PATH`
-echo "✅ Located all compartment id: $COMPARTMENT_ID"
+echo "✅ Located compartment id: $COMPARTMENT_ID"
+
+find_active_id_by_display_name() {
+ local display_name="$1"
+ jq -r --arg display_name "$display_name" 'first(.data[] | select(."display-name" == $display_name) | select((."lifecycle-state" // "") != "TERMINATED") | .id) // empty'
+}
+
+find_available_id_by_display_name() {
+ local display_name="$1"
+ jq -r --arg display_name "$display_name" 'first(.data[] | select(."display-name" == $display_name) | select((."lifecycle-state" // "") == "AVAILABLE") | .id) // empty'
+}
+
+find_active_bastion_id_by_name() {
+ local bastion_name="$1"
+ jq -r --arg bastion_name "$bastion_name" 'first(.data[] | select(.name == $bastion_name) | select((."lifecycle-state" // "") == "ACTIVE") | .id) // empty'
+}
+
+IOT_APPS_SUBNET_CIDR=${IOT_APPS_SUBNET_CIDR:-$SUBNET_CIDR}
+IOT_CLIENT_VM_SUBNET_CIDR=${IOT_CLIENT_VM_SUBNET_CIDR:-10.0.2.0/24}
+BASTION_CLIENT_CIDR_LIST=${BASTION_CLIENT_CIDR_LIST:-'["0.0.0.0/0"]'}
+DB_CLIENT_VM_CLOUD_INIT_FILE=${DB_CLIENT_VM_CLOUD_INIT_FILE:-../OCISetup/DBClientVMCloudInit.sh}
+INSTANCE_OPTIONS=$(jq -cn '{areLegacyImdsEndpointsDisabled: true}')
+SECURITY_LIST_EGRESS_RULES=$(jq -cn '[
+ {
+ "destination": "0.0.0.0/0",
+ "protocol": "all",
+ "description": "Allow all outbound traffic"
+ }
+]')
+SECURITY_LIST_INGRESS_RULES=$(jq -cn --arg vcn_cidr "$VCN_CIDR" '[
+ {
+ "source": $vcn_cidr,
+ "protocol": "6",
+ "tcpOptions": {
+ "destinationPortRange": {
+ "min": 1521,
+ "max": 1522
+ }
+ },
+ "description": "Allow Oracle Database access from VMs in the VCN"
+ },
+ {
+ "source": $vcn_cidr,
+ "protocol": "6",
+ "tcpOptions": {
+ "destinationPortRange": {
+ "min": 22,
+ "max": 22
+ }
+ },
+ "description": "Allow SSH access from the VCN"
+ },
+ {
+ "source": $vcn_cidr,
+ "protocol": "6",
+ "tcpOptions": {
+ "destinationPortRange": {
+ "min": 80,
+ "max": 80
+ }
+ },
+ "description": "Allow HTTP access from the VCN"
+ },
+ {
+ "source": $vcn_cidr,
+ "protocol": "6",
+ "tcpOptions": {
+ "destinationPortRange": {
+ "min": 443,
+ "max": 443
+ }
+ },
+ "description": "Allow HTTPS access from the VCN"
+ }
+]')
+
# --- 1. Network Infrastructure ---
SERVICE_ID=$(oci network service list --query "data[?contains(name, 'All ')].id | [0]" --raw-output)
-echo "✅ Located all services id: $SERVICE_ID"
-VCN_ID=$(oci network vcn create --compartment-id "$COMPARTMENT_ID" --display-name "$VCN_NAME" --cidr-block "$VCN_CIDR" --query "data.id" --raw-output)
-echo "✅ VCN Gateway Created: $VCN_ID"
-IG_ID=$(oci network internet-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "IGW" --is-enabled true --query "data.id" --raw-output)
-echo "✅ Internet Gateway Created: $ID_ID"
-# Create NAT Gateway for outgoing traffic from private resources
-NAT_ID=$(oci network nat-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "NAT_Gateway" --query "data.id" --raw-output)
-echo "✅ NAT Gateway Created: $NAT_ID"
-SG_ID=$(oci network service-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "SG" --services "[{\"serviceId\":\"$SERVICE_ID\"}]" --query "data.id" --raw-output)
-echo "✅ Service Gateway Created: $SG_ID"
-
-# --- 2. Security List with VCN-Wide DB Egress ---
-SL_ID=$(oci network security-list create \
- --compartment-id "$COMPARTMENT_ID" \
- --vcn-id "$VCN_ID" \
- --display-name "VCN_Wide_DB_Security" \
- --egress-security-rules "[
- {\"destination\":\"0.0.0.0/0\",\"protocol\":\"all\",\"description\":\"Allow all outbound for internet/updates\"},
- {\"destination\":\"$VCN_CIDR\",\"protocol\":\"6\",\"tcpOptions\":{\"destinationPortRange\":{\"max\":1522,\"min\":1521}},\"description\":\"Allow DB access to any resource in the VCN\"}
- ]" \
- --ingress-security-rules "[
- {\"source\":\"0.0.0.0/0\",\"protocol\":\"6\",\"tcpOptions\":{\"destinationPortRange\":{\"max\":22,\"min\":22}},\"description\":\"Public SSH Access\"},
- {\"source\":\"$SUBNET_CIDR\",\"protocol\":\"6\",\"tcpOptions\":{\"destinationPortRange\":{\"max\":1522,\"min\":1521}},\"description\":\"Allow DB traffic from this subnet\"}
- ]" --query "data.id" --raw-output)
-echo "✅ Security liss Created: $SL_ID"
-# --- 3. Routing and Subnet ---
-echo creating route table as oci network route-table create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "Public_RT" --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$NAT_ID\"}]" --query "data.id" --raw-output
-PUB_RT_ID=$(oci network route-table create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "Public_RT" --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$NAT_ID\"}]" --query "data.id" --raw-output)
-echo "✅ Public route table Created: $PUB_RT_ID"
-echo Creating subnet as oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "IoT_Subnet" --cidr-block "$SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --query "data.id" --raw-output
-PUB_SUBNET_ID=$(oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "IoT_Subnet" --cidr-block "$SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --query "data.id" --raw-output)
-echo "✅ Subnet Created: $PUB_SUBNET_ID"
-# --- 4. Compute Instance ---
-AD_NAME=$(oci iam availability-domain list --compartment-id "$COMPARTMENT_ID" --query "data[0].name" --raw-output)
-echo "Located ad name"
-IMAGE_ID=$(oci compute image list --compartment-id "$COMPARTMENT_ID" --operating-system "Oracle Linux" --operating-system-version "8" --shape "VM.Standard.E5.Flex" --query "data[0].id" --raw-output)
-echo "Located image id"
-echo "🛰️ Launching VM.Standard.E5.Flex (1 OCPU, 8GB RAM)..."
-
-INSTANCE_ID=$(oci compute instance launch \
- --compartment-id "$COMPARTMENT_ID" \
- --availability-domain "$AD_NAME" \
- --display-name "$VM_NAME" \
- --image-id "$IMAGE_ID" \
- --shape "VM.Standard.E5.Flex" \
- --shape-config "{\"ocpus\":1,\"memoryInGBs\":8}" \
- --subnet-id "$PUB_SUBNET_ID" \
- --assign-public-ip true \
- --ssh-authorized-keys-file "$SSH_PUBLIC_KEY_PATH" \
- --query "data.id" --raw-output)
-
-# --- 3. Final Output ---
-echo "⏳ Waiting for Public IP..."
-sleep 20
-PUBLIC_IP=$(oci compute instance list-vnics --instance-id "$INSTANCE_ID" --query "data[0].\"public-ip\"" --raw-output)
+echo "✅ Located all services id: $SERVICE_ID"
+
+VCN_ID=$(oci network vcn list --compartment-id "$COMPARTMENT_ID" --all | find_available_id_by_display_name "$VCN_NAME")
+if [[ -n "$VCN_ID" ]]
+then
+ echo "✅ Reusing existing VCN: $VCN_ID"
+else
+ VCN_ID=$(oci network vcn create --compartment-id "$COMPARTMENT_ID" --display-name "$VCN_NAME" --cidr-block "$VCN_CIDR" --query "data.id" --raw-output)
+ echo "✅ VCN Created: $VCN_ID"
+fi
+
+IG_ID=$(oci network internet-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "IGW")
+if [[ -n "$IG_ID" ]]
+then
+ echo "✅ Reusing existing Internet Gateway: $IG_ID"
+else
+ IG_ID=$(oci network internet-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "IGW" --is-enabled true --query "data.id" --raw-output)
+ echo "✅ Internet Gateway Created: $IG_ID"
+fi
+
+NAT_ID=$(oci network nat-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "NAT_Gateway")
+if [[ -n "$NAT_ID" ]]
+then
+ echo "✅ Reusing existing NAT Gateway: $NAT_ID"
+else
+ NAT_ID=$(oci network nat-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "NAT_Gateway" --query "data.id" --raw-output)
+ echo "✅ NAT Gateway Created: $NAT_ID"
+fi
+
+SG_ID=$(oci network service-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "SG")
+if [[ -n "$SG_ID" ]]
+then
+ echo "✅ Reusing existing Service Gateway: $SG_ID"
+else
+ SG_ID=$(oci network service-gateway create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "SG" --services "[{\"serviceId\":\"$SERVICE_ID\"}]" --query "data.id" --raw-output)
+ echo "✅ Service Gateway Created: $SG_ID"
+fi
+
+# --- 2. Security List ---
+SL_ID=$(oci network security-list list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "VCN_Wide_DB_Security")
+if [[ -n "$SL_ID" ]]
+then
+ echo "✅ Reusing existing Security List: $SL_ID"
+else
+ SL_ID=$(oci network security-list create \
+ --compartment-id "$COMPARTMENT_ID" \
+ --vcn-id "$VCN_ID" \
+ --display-name "VCN_Wide_DB_Security" \
+ --egress-security-rules "$SECURITY_LIST_EGRESS_RULES" \
+ --ingress-security-rules "$SECURITY_LIST_INGRESS_RULES" \
+ --query "data.id" --raw-output)
+ echo "✅ Security List Created: $SL_ID"
+fi
+
+oci network security-list update \
+ --security-list-id "$SL_ID" \
+ --egress-security-rules "$SECURITY_LIST_EGRESS_RULES" \
+ --ingress-security-rules "$SECURITY_LIST_INGRESS_RULES" \
+ --force >/dev/null
+echo "✅ Security List rules configured for DB, SSH, HTTP, and HTTPS access from $VCN_CIDR, and all outbound traffic"
+
+# --- 3. Routing and Private Subnets ---
+PUB_RT_ID=$(oci network route-table list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "Public_RT")
+if [[ -n "$PUB_RT_ID" ]]
+then
+ echo "✅ Reusing existing Public route table: $PUB_RT_ID"
+else
+ echo creating route table as oci network route-table create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "Public_RT" --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$NAT_ID\"}]" --query "data.id" --raw-output
+ PUB_RT_ID=$(oci network route-table create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "Public_RT" --route-rules "[{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$NAT_ID\"}]" --query "data.id" --raw-output)
+ echo "✅ Public route table Created: $PUB_RT_ID"
+fi
+
+IOT_APPS_SUBNET_ID=$(oci network subnet list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "$IOT_APPS_SUBNET")
+if [[ -n "$IOT_APPS_SUBNET_ID" ]]
+then
+ echo "✅ Reusing existing apps subnet: $IOT_APPS_SUBNET_ID"
+else
+ echo Creating apps subnet as oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "$IOT_APPS_SUBNET" --cidr-block "$IOT_APPS_SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --prohibit-public-ip-on-vnic true --query "data.id" --raw-output
+ IOT_APPS_SUBNET_ID=$(oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "$IOT_APPS_SUBNET" --cidr-block "$IOT_APPS_SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --prohibit-public-ip-on-vnic true --query "data.id" --raw-output)
+ echo "✅ Apps subnet Created: $IOT_APPS_SUBNET_ID"
+fi
+
+IOT_CLIENT_VM_SUBNET_ID=$(oci network subnet list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --all | find_available_id_by_display_name "$IOT_CLIENT_VM_SUBNET")
+if [[ -n "$IOT_CLIENT_VM_SUBNET_ID" ]]
+then
+ echo "✅ Reusing existing client VM subnet: $IOT_CLIENT_VM_SUBNET_ID"
+else
+ echo Creating client VM subnet as oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "$IOT_CLIENT_VM_SUBNET" --cidr-block "$IOT_CLIENT_VM_SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --prohibit-public-ip-on-vnic true --query "data.id" --raw-output
+ IOT_CLIENT_VM_SUBNET_ID=$(oci network subnet create --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" --display-name "$IOT_CLIENT_VM_SUBNET" --cidr-block "$IOT_CLIENT_VM_SUBNET_CIDR" --route-table-id "$PUB_RT_ID" --security-list-ids "[\"$SL_ID\"]" --prohibit-public-ip-on-vnic true --query "data.id" --raw-output)
+ echo "✅ Client VM subnet Created: $IOT_CLIENT_VM_SUBNET_ID"
+fi
+
+# --- 4. Bastion ---
+BASTION_ID=$(oci bastion bastion list --compartment-id "$COMPARTMENT_ID" --name "$BASTION_NAME" --all | find_active_bastion_id_by_name "$BASTION_NAME")
+if [[ -n "$BASTION_ID" ]]
+then
+ echo "✅ Reusing existing Bastion: $BASTION_ID"
+ oci bastion bastion update \
+ --bastion-id "$BASTION_ID" \
+ --client-cidr-list "$BASTION_CLIENT_CIDR_LIST" \
+ --force \
+ --wait-for-state SUCCEEDED >/dev/null
+ echo "✅ Bastion client CIDR allow list configured: $BASTION_CLIENT_CIDR_LIST"
+else
+ echo "Creating Bastion $BASTION_NAME for client VM subnet $IOT_CLIENT_VM_SUBNET_ID"
+ oci bastion bastion create \
+ --compartment-id "$COMPARTMENT_ID" \
+ --name "$BASTION_NAME" \
+ --bastion-type standard \
+ --target-subnet-id "$IOT_CLIENT_VM_SUBNET_ID" \
+ --client-cidr-list "$BASTION_CLIENT_CIDR_LIST" \
+ --wait-for-state SUCCEEDED >/dev/null
+ BASTION_ID=$(oci bastion bastion list --compartment-id "$COMPARTMENT_ID" --name "$BASTION_NAME" --all | find_active_bastion_id_by_name "$BASTION_NAME")
+ echo "✅ Bastion Created: $BASTION_ID"
+fi
+
+# --- 5. Compute Instance ---
+INSTANCE_ID=$(oci compute instance list --compartment-id "$COMPARTMENT_ID" --display-name "$VM_NAME" --all | find_active_id_by_display_name "$VM_NAME")
+if [[ -n "$INSTANCE_ID" ]]
+then
+ echo "✅ Reusing existing compute instance: $INSTANCE_ID"
+ oci compute instance update \
+ --instance-id "$INSTANCE_ID" \
+ --instance-options "$INSTANCE_OPTIONS" \
+ --force >/dev/null
+ echo "✅ Disabled legacy instance metadata endpoints"
+ echo "ℹ️ Existing compute instance reused; cloud-init runs only when a new VM is created"
+else
+ if [[ ! -f "$DB_CLIENT_VM_CLOUD_INIT_FILE" ]]
+ then
+ echo "❌ Cloud-init script not found: $DB_CLIENT_VM_CLOUD_INIT_FILE"
+ exit 1
+ fi
+
+ AD_NAME=$(oci iam availability-domain list --compartment-id "$COMPARTMENT_ID" --query "data[0].name" --raw-output)
+ echo "Located ad name"
+ IMAGE_ID=$(oci compute image list \
+ --compartment-id "$COMPARTMENT_ID" \
+ --operating-system "Oracle Linux" \
+ --operating-system-version "9" \
+ --shape "VM.Standard.E5.Flex" \
+ --sort-by TIMECREATED \
+ --sort-order DESC \
+ --query "data[0].id" --raw-output)
+ echo "Located image id"
+ echo "🛰️ Launching VM.Standard.E5.Flex with Oracle Linux 9 (1 OCPU, 8GB RAM)..."
+
+ INSTANCE_ID=$(oci compute instance launch \
+ --compartment-id "$COMPARTMENT_ID" \
+ --availability-domain "$AD_NAME" \
+ --display-name "$VM_NAME" \
+ --image-id "$IMAGE_ID" \
+ --shape "VM.Standard.E5.Flex" \
+ --shape-config "{\"ocpus\":1,\"memoryInGBs\":8}" \
+ --subnet-id "$IOT_CLIENT_VM_SUBNET_ID" \
+ --assign-public-ip false \
+ --instance-options "$INSTANCE_OPTIONS" \
+ --user-data-file "$DB_CLIENT_VM_CLOUD_INIT_FILE" \
+ --ssh-authorized-keys-file "$SSH_PUBLIC_KEY_PATH" \
+ --query "data.id" --raw-output)
+
+ echo "⏳ Waiting for VNIC..."
+ sleep 20
+fi
+
+# --- 6. Final Output ---
+VNIC_JSON=$(oci compute instance list-vnics --instance-id "$INSTANCE_ID")
+PRIVATE_IP=$(echo "$VNIC_JSON" | jq -r '(first(.data[]) | ."private-ip") // empty')
+PUBLIC_IP=$(echo "$VNIC_JSON" | jq -r '(first(.data[]) | ."public-ip") // empty')
+if [[ -z "$PUBLIC_IP" || "$PUBLIC_IP" == "null" ]]
+then
+ PUBLIC_IP="none (private subnet)"
+fi
echo "------------------------------------------------"
-echo "✅ Infrastructure Deployed"
+echo "✅ Infrastructure Ready"
+echo "Apps Subnet: $IOT_APPS_SUBNET_ID"
+echo "Client VM Subnet: $IOT_CLIENT_VM_SUBNET_ID"
+echo "Bastion: $BASTION_ID"
+echo "Cloud Init Script: $DB_CLIENT_VM_CLOUD_INIT_FILE"
+echo "Private IP: $PRIVATE_IP"
echo "Public IP: $PUBLIC_IP"
-echo "Shape: VM.Standard.E5.Flex (1 OCPU / 1GB RAM)"
+echo "Shape: VM.Standard.E5.Flex (1 OCPU / 8GB RAM)"
echo "------------------------------------------------"
-echo "✅ Deployment Complete. The VM can now initiate connections to any DB on ports 1521-1522 within $VCN_CIDR."
\ No newline at end of file
+echo "✅ Deployment Complete. The VM can now initiate connections to any DB on ports 1521-1522 within $VCN_CIDR."
diff --git a/IoTSonnenUploader/OCISetup/common_names.sh b/IoTSonnenUploader/OCISetup/common_names.sh
index 6f3fae3..06b0140 100755
--- a/IoTSonnenUploader/OCISetup/common_names.sh
+++ b/IoTSonnenUploader/OCISetup/common_names.sh
@@ -37,8 +37,11 @@ export DIGITAL_TWIN_MODEL_NAME=homebattery
export DIGITAL_TWIN_MODEL_FILE_NAME=HomeBatteryDTMI.json
# note that this is a relative path
export DIGITAL_TWIN_MODEL_FILE=file://$DIGITAL_TWIN_MODEL_FILE_NAME
-# the models name we extract from the model file
-export DIGITAL_TWIN_MODEL_IDENTIFIER=`cat $DIGITAL_TWIN_MODEL_FILE_NAME | jq -r '.["@id"]'`
+# the models name we extract from the model file,
+# As the common_names.sh script is used all over in different directories
+# that has been disbaled for now due to directory locations, but if needed use the following in your script
+# (adjusting for directories of course !)
+# export DIGITAL_TWIN_MODEL_IDENTIFIER=`cat $DIGITAL_TWIN_MODEL_FILE_NAME | jq -r '.["@id"]'`
# values for the adapters
export DIGITAL_TWIN_ADAPTOR_ROUTE_MAPPINGS_FILE=file://sonnen-to-generic-mapping-multiple-routes.json
export DIGITAL_TWIN_ADAPTOR_ENVELOPE_MAPPINGS_FILE=file://sonnen-to-generic-mapping-envelope.json
@@ -55,6 +58,10 @@ export SSH_PUBLIC_KEY_PATH="$HOME/.ssh/id_rsa.pub"
export VCN_NAME="iotdemosvcn"
export VCN_CIDR="10.0.0.0/16"
export SUBNET_CIDR="10.0.1.0/24"
+export IOT_APPS_SUBNET="iotclientapps"
+export IOT_APPS_SUBNET_CIDR="$SUBNET_CIDR"
+export IOT_CLIENT_VM_SUBNET="iotclientvms"
+export IOT_CLIENT_VM_SUBNET_CIDR="10.0.2.0/24"
# the name of the IDCS instance for the dynamic group of the compute VMs - can differ from the ORDS OAUTH IDCS instance name
export IOT_CLIENT_VM_DYNAMIC_GROUP_IDCS_NAME=OracleIdentityCloudService
@@ -64,8 +71,10 @@ export IOT_CLIENT_VM_DYNAMIC_GROUP_NAME=TGIoTDBAccessDG
# Virtual machine access to the IOT Instance
export VM_NAME="iotdbaccess"
export VM_USER_NAME="opc"
+export DB_CLIENT_VM_CLOUD_INIT_FILE=../OCISetup/DBClientVMCloudInit.sh
# allows us to connect to it
export BASTION_NAME=IoTDBAccessBastion
+export BASTION_CLIENT_CIDR_LIST='["0.0.0.0/0"]'
export BASTION_PRIV_KEY_PATH="$HOME/.ssh/id_rsa"
export BASTION_PUB_KEY_PATH="$BASTION_PRIV_KEY_PATH"".pub"