From 159a390822e44dadbe8596781ff54104593fcdf5 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 11 Jun 2026 15:23:34 +0200 Subject: [PATCH 01/10] Bridge EV-charger MQTT to S2 FRBC in Ruby The reference implementation: a Rails app that consumes charger telemetry and lifecycle events from an MQTT broker, translates each message to its S2 counterpart over a WebSocket session (FRBC control type), and turns inbound FRBC.Instruction messages back into MQTT power setpoints. Derived from the S2 bridge Stekker.app runs against its smart-charging platform; the S2 wire types come from the public s2-ruby gem, pinned to an exact revision. Runs stand-alone with docker compose and ships its spec suite. Co-authored-by: Bob Forma Co-authored-by: Erik de Bruijn Co-authored-by: Martijn Versluis --- resource-manager-mqtt-bridge-ruby/README.md | 438 ++++++++++ .../rm/.dockerignore | 14 + .../rm/.gitattributes | 9 + .../rm/.gitignore | 17 + resource-manager-mqtt-bridge-ruby/rm/.rspec | 1 + .../rm/.ruby-version | 1 + .../rm/Dockerfile | 77 ++ resource-manager-mqtt-bridge-ruby/rm/Gemfile | 37 + .../rm/Gemfile.lock | 433 ++++++++++ resource-manager-mqtt-bridge-ruby/rm/Rakefile | 6 + .../app/controllers/application_controller.rb | 27 + .../rm/app/controllers/concerns/.keep | 0 .../app/controllers/resources_controller.rb | 15 + .../rm/app/controllers/traffic_controller.rb | 32 + .../rm/app/helpers/application_helper.rb | 7 + .../rm/app/jobs/application_job.rb | 7 + .../app/jobs/process_frbc_instruction_job.rb | 66 ++ .../jobs/publish_s2_ready_notification_job.rb | 22 + .../send_heartbeat_power_measurement_job.rb | 32 + .../send_heartbeat_power_measurements_job.rb | 9 + .../rm/app/lib/base_factory.rb | 10 + .../rm/app/lib/connection_registry.rb | 34 + .../rm/app/lib/error_notifier.rb | 10 + .../lib/frbc_system_description_factory.rb | 125 +++ .../rm/app/lib/instruction_status_tracker.rb | 15 + .../rm/app/lib/message_relayer.rb | 18 + .../rm/app/lib/mqtt_message_handler.rb | 174 ++++ .../rm/app/lib/mqtt_message_logger.rb | 55 ++ .../rm/app/lib/reference_config.rb | 44 + .../lib/resource_manager_details_factory.rb | 32 + .../rm/app/lib/s2_message_handler.rb | 109 +++ .../rm/app/lib/s2_message_logger.rb | 41 + .../rm/app/lib/stekker_formatter.rb | 33 + .../rm/app/lib/supervisor.rb | 308 +++++++ .../rm/app/lib/traffic_log.rb | 48 ++ .../rm/app/models/application_record.rb | 3 + .../rm/app/models/concerns/.keep | 0 .../rm/app/models/resource.rb | 16 + .../log_mqtt_listener_subscriber.rb | 56 ++ .../log_mqtt_message_handler_subscriber.rb | 8 + .../app/subscribers/log_session_subscriber.rb | 67 ++ .../rm/app/views/layouts/bridge.html.erb | 12 + .../rm/app/views/resources/index.html.erb | 133 +++ .../rm/bin/bundle | 113 +++ .../rm/bin/docker-entrypoint | 14 + resource-manager-mqtt-bridge-ruby/rm/bin/jobs | 6 + .../rm/bin/rails | 4 + resource-manager-mqtt-bridge-ruby/rm/bin/rake | 4 + .../rm/config.ru | 6 + .../rm/config/application.rb | 50 ++ .../rm/config/boot.rb | 4 + .../rm/config/cable.yml | 14 + .../rm/config/cache.yml | 15 + .../rm/config/database.yml | 87 ++ .../rm/config/environment.rb | 5 + .../rm/config/environments/development.rb | 71 ++ .../rm/config/environments/production.rb | 84 ++ .../rm/config/environments/test.rb | 54 ++ .../rm/config/initializers/assets.rb | 7 + .../initializers/content_security_policy.rb | 29 + .../initializers/filter_parameter_logging.rb | 8 + .../rm/config/initializers/inflections.rb | 16 + .../rm/config/initializers/instrumentation.rb | 10 + .../new_framework_defaults_8_1.rb | 75 ++ .../rm/config/initializers/s2.rb | 20 + .../rm/config/puma.rb | 42 + .../rm/config/queue.yml | 18 + .../rm/config/recurring.yml | 18 + .../rm/config/routes.rb | 10 + .../20250908135623_create_resources.rb | 10 + .../20250923094344_setup_solid_cable.rb | 13 + .../20250923094447_setup_solid_queue.rb | 135 ++++ .../20250923094521_setup_solid_cache.rb | 14 + ...emove_stekker_identifier_from_resources.rb | 5 + ...002095142_add_configuration_to_resource.rb | 5 + ...dd_uniqueness_to_resource_s2_identifier.rb | 5 + ...2110554_add_disconnected_at_to_resource.rb | 5 + ..._last_power_measurement_at_to_resources.rb | 5 + .../rm/db/schema.rb | 175 ++++ .../rm/db/seeds.rb | 0 .../rm/docker-compose.yml | 47 ++ .../rm/examples/charge_status.json | 8 + .../rm/examples/charge_target.json | 10 + .../rm/examples/connect.json | 11 + .../rm/examples/disconnect.json | 4 + .../rm/examples/instruction_update.json | 9 + .../rm/examples/power_forecast.json | 22 + .../rm/examples/power_measurement.json | 9 + .../rm/lib/tasks/.keep | 0 .../rm/log/.keep | 0 .../rm/mosquitto.conf | 2 + .../rm/public/400.html | 120 +++ .../rm/public/404.html | 120 +++ .../rm/public/406-unsupported-browser.html | 120 +++ .../rm/public/422.html | 120 +++ .../rm/public/500.html | 120 +++ .../rm/public/icon.png | Bin 0 -> 30367 bytes .../rm/public/icon.svg | 12 + .../rm/public/robots.txt | 1 + .../rm/public/tokens.css | 174 ++++ .../rm/script/.keep | 0 .../rm/spec/factories/resources.rb | 32 + .../factories/s2/messages/frbc/instruction.rb | 15 + .../spec/factories/s2/messages/handshake.rb | 17 + .../s2/messages/handshake_response.rb | 9 + .../factories/s2/messages/reception_status.rb | 33 + .../s2/messages/select_control_type.rb | 39 + .../rm/spec/factories/uuid.rb | 5 + .../jobs/process_frbc_instruction_job_spec.rb | 156 ++++ .../publish_s2_ready_notification_job_spec.rb | 21 + ...nd_heartbeat_power_measurement_job_spec.rb | 26 + ...d_heartbeat_power_measurements_job_spec.rb | 39 + .../rm/spec/lib/base_factory_spec.rb | 12 + .../rm/spec/lib/error_notifier_spec.rb | 35 + .../lib/instruction_status_tracker_spec.rb | 53 ++ .../rm/spec/lib/message_relayer_spec.rb | 50 ++ .../rm/spec/lib/mqtt_message_handler_spec.rb | 490 ++++++++++++ .../rm/spec/lib/mqtt_message_logger_spec.rb | 58 ++ .../rm/spec/lib/s2/connection_spec.rb | 65 ++ .../rm/spec/lib/s2/message_factory_spec.rb | 72 ++ .../s2/message_handler/dispatching_spec.rb | 118 +++ .../s2/message_handler/error_handling_spec.rb | 234 ++++++ .../lib/s2/message_handler/replying_spec.rb | 159 ++++ .../rm/spec/lib/s2/message_handler_spec.rb | 84 ++ .../rm/spec/lib/s2/message_sender_spec.rb | 107 +++ .../rm/spec/lib/s2/session_spec.rb | 126 +++ .../rm/spec/lib/s2_message_handler_spec.rb | 572 +++++++++++++ .../rm/spec/lib/s2_message_logger_spec.rb | 52 ++ .../rm/spec/lib/supervisor_spec.rb | 472 +++++++++++ .../rm/spec/lib/traffic_log_spec.rb | 21 + .../rm/spec/models/resource_spec.rb | 22 + .../rm/spec/rails_helper.rb | 34 + .../rm/spec/requests/resources_spec.rb | 48 ++ .../rm/spec/requests/traffic_spec.rb | 22 + .../rm/spec/stekker_s2/application_spec.rb | 5 + .../log_mqtt_listener_subscriber_spec.rb | 188 +++++ ...og_mqtt_message_handler_subscriber_spec.rb | 20 + .../log_session_subscriber_spec.rb | 210 +++++ .../rm/spec/support/async.rb | 10 + .../rm/spec/support/factory_bot.rb | 3 + .../rm/spec/support/fake_mqtt_client.rb | 12 + .../rm/spec/support/fake_websocket.rb | 40 + .../rm/spec/support/message_sender_stub.rb | 17 + .../rm/spec/support/shoulda_matchers.rb | 6 + .../rm/spec/support/websocket_server.rb | 87 ++ .../rm/vendor/.keep | 0 .../rm/vendor/stekker-mqtt/.gitignore | 9 + .../rm/vendor/stekker-mqtt/.rspec | 1 + .../rm/vendor/stekker-mqtt/.ruby-version | 1 + .../rm/vendor/stekker-mqtt/Gemfile | 10 + .../rm/vendor/stekker-mqtt/Gemfile.lock | 95 +++ .../rm/vendor/stekker-mqtt/LICENSE.txt | 21 + .../rm/vendor/stekker-mqtt/README.md | 3 + .../rm/vendor/stekker-mqtt/Rakefile | 1 + .../rm/vendor/stekker-mqtt/bin/console | 11 + .../vendor/stekker-mqtt/bin/generate_messages | 11 + .../rm/vendor/stekker-mqtt/bin/setup | 8 + .../lib/json-schema/charge_status.schema.json | 50 ++ .../lib/json-schema/charge_target.schema.json | 60 ++ .../lib/json-schema/common.schema.json | 30 + .../lib/json-schema/connect.schema.json | 76 ++ .../lib/json-schema/disconnect.schema.json | 26 + .../instruction_update.schema.json | 66 ++ .../json-schema/power_forecast.schema.json | 83 ++ .../json-schema/power_measurement.schema.json | 56 ++ .../json-schema/power_setpoint.schema.json | 68 ++ .../s2_ready_notification.schema.json | 26 + .../vendor/stekker-mqtt/lib/stekker/mqtt.rb | 14 + .../lib/stekker/mqtt/factories.rb | 7 + .../lib/stekker/mqtt/fake_client.rb | 24 + .../stekker-mqtt/lib/stekker/mqtt/listener.rb | 108 +++ .../lib/stekker/mqtt/message_factory.rb | 50 ++ .../lib/stekker/mqtt/message_handler.rb | 62 ++ .../stekker-mqtt/lib/stekker/mqtt/messages.rb | 757 ++++++++++++++++++ .../lib/stekker/mqtt/publisher.rb | 35 + .../lib/stekker/mqtt/rspec/matchers.rb | 94 +++ .../stekker-mqtt/lib/stekker/mqtt/version.rb | 5 + .../spec/factories/charge_status.rb | 17 + .../spec/factories/charge_target.rb | 19 + .../stekker-mqtt/spec/factories/connect.rb | 20 + .../stekker-mqtt/spec/factories/disconnect.rb | 10 + .../spec/factories/instruction_update.rb | 18 + .../spec/factories/power_forecast.rb | 22 + .../spec/factories/power_measurement.rb | 18 + .../spec/factories/power_setpoint.rb | 19 + .../spec/factories/s2_ready_notification.rb | 10 + .../vendor/stekker-mqtt/spec/spec_helper.rb | 26 + .../spec/stekker/mqtt/listener_spec.rb | 126 +++ .../spec/stekker/mqtt/message_factory_spec.rb | 90 +++ .../spec/stekker/mqtt/message_handler_spec.rb | 66 ++ .../spec/stekker/mqtt/publisher_spec.rb | 129 +++ .../stekker-mqtt/spec/support/factory_bot.rb | 7 + .../vendor/stekker-mqtt/stekker-mqtt.gemspec | 30 + 193 files changed, 11057 insertions(+) create mode 100644 resource-manager-mqtt-bridge-ruby/README.md create mode 100644 resource-manager-mqtt-bridge-ruby/rm/.dockerignore create mode 100644 resource-manager-mqtt-bridge-ruby/rm/.gitattributes create mode 100644 resource-manager-mqtt-bridge-ruby/rm/.gitignore create mode 100644 resource-manager-mqtt-bridge-ruby/rm/.rspec create mode 100644 resource-manager-mqtt-bridge-ruby/rm/.ruby-version create mode 100644 resource-manager-mqtt-bridge-ruby/rm/Dockerfile create mode 100644 resource-manager-mqtt-bridge-ruby/rm/Gemfile create mode 100644 resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock create mode 100644 resource-manager-mqtt-bridge-ruby/rm/Rakefile create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/controllers/concerns/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/models/concerns/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb create mode 100755 resource-manager-mqtt-bridge-ruby/rm/bin/bundle create mode 100755 resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint create mode 100755 resource-manager-mqtt-bridge-ruby/rm/bin/jobs create mode 100755 resource-manager-mqtt-bridge-ruby/rm/bin/rails create mode 100755 resource-manager-mqtt-bridge-ruby/rm/bin/rake create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config.ru create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/application.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/boot.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/cable.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/cache.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/database.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/environment.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/puma.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/queue.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/config/routes.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/schema.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/db/seeds.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/connect.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/lib/tasks/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/log/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/400.html create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/404.html create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/422.html create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/500.html create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/icon.png create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/icon.svg create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/robots.txt create mode 100644 resource-manager-mqtt-bridge-ruby/rm/public/tokens.css create mode 100644 resource-manager-mqtt-bridge-ruby/rm/script/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/.keep create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile create mode 100755 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console create mode 100755 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages create mode 100755 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb create mode 100644 resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec diff --git a/resource-manager-mqtt-bridge-ruby/README.md b/resource-manager-mqtt-bridge-ruby/README.md new file mode 100644 index 0000000..02f4076 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/README.md @@ -0,0 +1,438 @@ +# S2 Resource Manager MQTT Bridge (Ruby) + +An **S2 Resource Manager (RM)** reference implementation. It is a Rails app that consumes resource-side MQTT +messages, stores the resource state, and translates each message into an [S2 protocol](https://s2standard.org/) +message that it relays over a WebSocket to a **CEM** (Customer Energy Manager) peer. It also turns the CEM's +`FRBC.Instruction` back into a Stekker `power_setpoint` on MQTT. + +The RM is the reference implementation itself. It lives in [`rm/`](rm/) and is the subject of this +document. + +Built by [Stekker.app B.V.](https://stekker.com), the team behind the Stekker smart-charging platform. + +## What this demonstrates + +Each resource-side MQTT event is parsed, stored when needed, mapped to the correct S2 message type, +serialized as S2 JSON, and handed to the WebSocket relay; an inbound S2 instruction becomes a concrete +MQTT power setpoint. + +- **Forward bridge path:** MQTT `power_measurements`, `power_forecasts`, `charge_statuses`, `charge_targets`, + and `instruction_updates` become S2 protocol messages with the expected `message_type`. +- **Resource lifecycle:** MQTT `connects` registers `demo-resource-001` and starts the RM WebSocket + lifecycle; MQTT `disconnects` marks it inactive and closes the connection. +- **Reverse bridge path:** an inbound S2 `FRBC.Instruction` becomes an MQTT `power_setpoint` plus an + `instruction_update` acknowledgment (see [Reverse path](#reverse-path-s2--mqtt)). + +You can see the forward path on its own by running the RM standalone (it falls back to a placeholder CEM). + +## Roles: this app is the Resource Manager + +In S2 there are two peers. This app is the **Resource Manager (RM)**: it represents an energy resource (here, +an EV charger) and speaks S2 on its behalf. The **CEM** is the controlling peer. The RM opens the WebSocket, +completes the handshake with `role: RM`, asserts the peer is `role: CEM`, and sends a `ResourceManagerDetails` +message. The CEM sends `FRBC.Instruction` messages back. + +All S2 knowledge lives in this app: it maps each inbound MQTT message to an `S2::Messages::*` object, +synthesizes the `FRBC.SystemDescription` and `ResourceManagerDetails` from the stored resource configuration, +and turns an inbound `FRBC.Instruction` back into a Stekker `power_setpoint`. The S2 wire types, schemas and +the `S2::Connection` WebSocket class come from the public +[`stekker/s2-ruby`](https://github.com/stekker/s2-ruby) gem, pinned to an exact revision. + +```mermaid +flowchart LR + client["Generic MQTT client
(mosquitto_pub)"] -->|publish| broker[("Mosquitto
broker")] + broker -->|"stekker/resources/+/…"| app["Rails S2 Resource
Manager (RM)"] + app --- db[("Postgres")] + app -->|"S2 over WebSocket"| cem["CEM
(placeholder when standalone)"] + browser["Browser"] -->|"reads the read-only dashboard"| app +``` + +A generic MQTT client publishes resource events to the broker; the bridge consumes them through its single +MQTT listener and relays each one as S2. The browser only reads the dashboard, which is a read-only view of +the live traffic and the resource state; it never publishes. + +## Run the Resource Manager standalone + +You need Docker with Compose. From [`rm/`](rm/): + +```sh +cd rm +docker compose up --build +``` + +This starts three services under the compose project `resource-manager-mqtt-ruby`: + +- **app**: the Rails RM bridge, on http://localhost:3000 +- **postgres**: PostgreSQL 16 (resource registry and Solid Queue/Cache/Cable) +- **mosquitto**: Eclipse Mosquitto 2 MQTT broker on port 1883 + +In another terminal: + +```sh +curl -fsS http://localhost:3000/up # => HTTP/1.1 200 OK +open http://localhost:3000 # read-only bridge dashboard, no login by default +``` + +When the RM runs standalone, there is no external CEM. The configured CEM URL is the local placeholder `ws://localhost:9999/s2`, so +the app shows MQTT ingest and S2 message construction, then logs that no CEM connection is +available. That is the expected offline result, not a failure. + +The app boots cleanly on an empty database: the supervisor only connects resources that already exist, and there +are none until you publish a `connect`. The dashboard has no login by default; set both `BASIC_AUTH_USERNAME` +and `BASIC_AUTH_PASSWORD` to require HTTP basic auth. + +### Port conflicts + +The standalone stack binds host ports `3000` (app) and `1883` (broker). If either is taken, add a local +`rm/docker-compose.override.yml` next to the compose file (it keeps the same internal ports): + +```yaml +services: + app: + ports: + - "3001:3000" + mosquitto: + ports: + - "1884:1883" +``` + +## Drive the bridge + +Publish the bundled examples with a generic MQTT client; the dashboard is read-only and never publishes. +**Publish `connect` first**: it registers the resource and triggers the WebSocket connect. Send `disconnect` +last. + +From `rm/`, `mosquitto_pub` inside the broker container is enough (the `./examples` folder is mounted at +`/examples`): + +```sh +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/connects -f /examples/connect.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/power_measurements -f /examples/power_measurement.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/power_forecasts -f /examples/power_forecast.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/charge_statuses -f /examples/charge_status.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/charge_targets -f /examples/charge_target.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/instruction_updates -f /examples/instruction_update.json +docker compose exec mosquitto mosquitto_pub -t stekker/resources/demo-resource-001/disconnects -f /examples/disconnect.json +``` + +Watch the translation on stdout: + +```sh +docker compose logs -f app +``` + +Each `mosquitto_pub` also appears in the dashboard's "RM messages" feed at http://localhost:3000. + +## MQTT topics and S2 mapping + +Every bundled example is a real resource-side message the bridge already supports. The S2 column is the +protocol `message_type` of the message the bridge builds and relays. + +| Example file | Topic | S2 message built | Direction | +| --- | --- | --- | --- | +| `connect.json` | `stekker/resources/demo-resource-001/connects` | (lifecycle) opens the WebSocket; `ResourceManagerDetails` after the handshake | MQTT → S2 | +| `power_measurement.json` | `stekker/resources/demo-resource-001/power_measurements` | `PowerMeasurement` | MQTT → S2 | +| `power_forecast.json` | `stekker/resources/demo-resource-001/power_forecasts` | `PowerForecast` | MQTT → S2 | +| `charge_status.json` | `stekker/resources/demo-resource-001/charge_statuses` | `FRBC.StorageStatus` | MQTT → S2 | +| `charge_target.json` | `stekker/resources/demo-resource-001/charge_targets` | `FRBC.FillLevelTargetProfile` | MQTT → S2 | +| `instruction_update.json` | `stekker/resources/demo-resource-001/instruction_updates` | `InstructionStatusUpdate` | MQTT → S2 | +| `disconnect.json` | `stekker/resources/demo-resource-001/disconnects` | (lifecycle) marks the resource inactive and closes the WebSocket | MQTT → S2 | +| (reverse path) | `power_setpoints` + `instruction_updates` | from inbound `FRBC.Instruction` | S2 → MQTT | + +The topic shape is `stekker/resources//`. The `resource_id` is read from the message +body; the bundled examples all use the same demo id `demo-resource-001`. + +## Expected output (forward path, static examples) + +Standalone, the RM proves the forward half of the bridge: inbound MQTT handling and S2 message construction. +With no real CEM behind the WebSocket, the bridge connects to the placeholder CEM (`ws://localhost:9999/s2`), +the connection is refused, and it retries with backoff. After each translation the relay logs that no +connected connection was found. The app stays healthy throughout. That is the expected caveat, not a failure. + +Each translated example logs one pair of lines in `docker compose logs app`: + +```text +[MessageRelayer] Built S2 for resource_id demo-resource-001: +[MessageRelayer] No connected connection found for resource_id demo-resource-001 +``` + +The S2 payloads below are **normalized**: the generated `message_id` UUID is shown as ``, and +container IDs, host ports and machine paths are removed. The static timestamps come straight from the example +JSON. + +Boot and health: + +```text +[Initializer] Starting Supervisor +[LogMQTTListenerSubscriber] MQTT listener connected +GET /up returns 200 +``` + +Connect (lifecycle; standalone the placeholder CEM refuses the connection and the bridge retries): + +```text +Inbound MQTT message ... stekker/resources/demo-resource-001/connects +Errno::ECONNREFUSED: Connection refused - connect(2) for 127.0.0.1:9999 +[Supervisor] [demo-resource-001] Reconnecting in 5s +[S2::Connection] [demo-resource-001] Reconnecting in 5s... +``` + +Power measurement → `PowerMeasurement`: + +```json +{ + "message_id": "", + "message_type": "PowerMeasurement", + "measurement_timestamp": "2026-06-04T10:00:00Z", + "values": [ + { "commodity_quantity": "ELECTRIC.POWER.3_PHASE_SYMMETRIC", "value": 3700 } + ] +} +``` + +Power forecast → `PowerForecast`: + +```json +{ + "message_id": "", + "message_type": "PowerForecast", + "start_time": "2026-06-04T10:00:00Z", + "elements": [ + { "duration": 900000, "power_values": [{ "commodity_quantity": "ELECTRIC.POWER.3_PHASE_SYMMETRIC", "value_expected": 3700 }] }, + { "duration": 1800000, "power_values": [{ "commodity_quantity": "ELECTRIC.POWER.3_PHASE_SYMMETRIC", "value_expected": 7400 }] }, + { "duration": 83700000, "power_values": [{ "commodity_quantity": "ELECTRIC.POWER.3_PHASE_SYMMETRIC", "value_expected": 0 }] } + ] +} +``` + +Each `power_values` entry also carries the optional S2 statistical fields (`value_lower_68PPR`, +`value_upper_limit`, and so on); they are present and `null` here and are omitted above for readability. + +Charge status → `FRBC.StorageStatus`: + +```json +{ + "message_id": "", + "message_type": "FRBC.StorageStatus", + "measurement_timestamp": "2026-06-04T10:05:00Z", + "present_fill_level": 42.5 +} +``` + +Charge target → `FRBC.FillLevelTargetProfile`: + +```json +{ + "message_id": "", + "message_type": "FRBC.FillLevelTargetProfile", + "start_time": "2026-06-04T10:00:00Z", + "elements": [ + { "duration": 27900000, "fill_level_range": { "start_of_range": 25.0, "end_of_range": 85.0 } }, + { "duration": 900000, "fill_level_range": { "start_of_range": 85.0, "end_of_range": 85.0 } } + ] +} +``` + +Instruction update → `InstructionStatusUpdate`: + +```json +{ + "message_id": "", + "message_type": "InstructionStatusUpdate", + "instruction_id": "demo-instruction-001", + "status_type": "SUCCEEDED", + "timestamp": "2026-06-04T10:10:00Z" +} +``` + +## Reverse path (S2 → MQTT) + +When a CEM sends an `FRBC.Instruction`, `S2MessageHandler` validates it against the cached +`FRBC.SystemDescription` and enqueues `ProcessFRBCInstructionJob` (`app/jobs/process_frbc_instruction_job.rb`). +The job: + +1. Resolves the instructed actuator and operation mode from the system description. +2. Computes the power setpoint in watts as `min_power + (max_power - min_power) * operation_mode_factor`, where + `min_power`/`max_power` are the instructed operation mode's power range. +3. Publishes a Stekker `power_setpoint` on `stekker/resources//power_setpoints`. +4. Publishes an `instruction_update` acknowledgment (`status: ACCEPTED`) on + `stekker/resources//instruction_updates`. + +The reverse path is **not exercised live** when the RM runs standalone. Below it is shown as a static, +normalized example, also covered by the in-tree spec. + +Inbound S2 `FRBC.Instruction` (representative fields): + +```json +{ + "message_type": "FRBC.Instruction", + "operation_mode": "", + "operation_mode_factor": 0.5, + "execution_time": "2024-01-01T12:00:00Z", + "duration": 600000 +} +``` + +Outbound Stekker `power_setpoint` (MQTT): + +```json +{ + "message_type": "power_setpoint", + "resource_id": "demo-resource-001", + "data": { + "reference": "", + "starts_at": "2024-01-01T12:00:00Z", + "ends_at": "2024-01-01T12:10:00Z", + "unit": "W", + "value": 5520 + } +} +``` + +Outbound Stekker `instruction_update` (MQTT): + +```json +{ + "message_type": "instruction_update", + "resource_id": "demo-resource-001", + "data": { + "reference": "", + "updated_at": "", + "status": "ACCEPTED" + } +} +``` + +The watt value depends only on the operation mode and the `operation_mode_factor`. The cases below are the +exact values asserted by `spec/jobs/process_frbc_instruction_job_spec.rb` (3 of its 4 examples; the 4th +asserts the `instruction_update` acknowledgment), which is the in-tree proof of this path and passes +against the pinned `s2-ruby` revision: + +| Operation mode | `operation_mode_factor` | Power floor | Output `value` (W) | +| --- | --- | --- | --- | +| Charging | 0.5 | none | 5520 | +| Charging | 0.5 | `minimum_current = 6` | 7590 | +| Off | 1.0 | none | 0 | + +## Connecting to a real CEM + +The placeholder CEM is only the standalone default. You can point this RM at a real CEM, for example in an +interoperability test with another party's CEM. + +> **CEM routing requirement.** For each active resource the RM opens one WebSocket to `CEM_BASE_URL` with the +> resource id appended as a trailing path segment, for example `wss://your-cem.example/s2/demo-resource-001`. +> The CEM must accept connections on the base path and read the resource id from that trailing segment. The +> `resource_id` is the one from the resource's MQTT message body (the same id used in the topics). + +Point the RM at the CEM and, if needed, the shared broker: + +```sh +CEM_BASE_URL=wss://your-cem.example/s2 +CEM_USERNAME=... # optional, HTTP Basic +CEM_PASSWORD=... # optional, HTTP Basic +MQTT_URL=mqtt://your-broker:1883 +``` + +Use `wss://` against a real CEM: HTTP Basic credentials travel in the WebSocket upgrade request, so `ws://` +sends them in the clear. + +Once connected, the RM completes the S2 handshake with `role: RM` and expects the peer to announce `role: CEM`. +It declares support for `FillRateBasedControl` in `ResourceManagerDetails`; the CEM selects it with +`SelectControlType`, and the RM then sends an `FRBC.SystemDescription`. When the CEM sends an +`FRBC.Instruction`, the RM translates it to a Stekker `power_setpoint` on MQTT. A successful connection shows +up in `docker compose logs app`: + +```text +[LogSessionSubscriber] [] S2 Session started +Outbound S2 message ... "message_type":"ResourceManagerDetails" +Outbound S2 message ... "message_type":"FRBC.SystemDescription" +Inbound S2 message ... "message_type":"FRBC.Instruction" +``` + +If the CEM runs in another container or on another host, the app container needs a route to it. Add a local +`rm/docker-compose.override.yml`. To reach another CEM container on a shared Docker network: + +```yaml +services: + app: + environment: + CEM_BASE_URL: "ws://other-cem:9000/s2" # ws:// is fine on a private network; use wss:// for a real CEM + networks: [s2demo] +networks: + s2demo: + external: true # create it once with: docker network create s2demo +``` + +To reach a CEM listening on the host instead: + +```yaml +services: + app: + environment: + CEM_BASE_URL: "ws://host.docker.internal:9000/s2" + extra_hosts: ["host.docker.internal:host-gateway"] # required on Linux Docker; harmless on Docker Desktop +``` + +**Interop surface (what this RM supports today).** It speaks the `FillRateBasedControl` (FRBC) profile only; +it authenticates to the CEM with HTTP Basic only; and it points every resource at one CEM endpoint +(`CEM_BASE_URL`) rather than routing different resources to different CEMs (a runtime simplification, not a +protocol requirement). It also keeps the [conformance workarounds](#conformance-notes) below. A CEM that +expects a different control type, a different auth scheme, or fields this RM does not send will not complete +the handshake. + +## Configuration + +All configuration is read from environment variables in one place +([`rm/app/lib/reference_config.rb`](rm/app/lib/reference_config.rb)). The compose file sets sensible demo +defaults; there are no secret-bearing files in this package. + +| ENV | Default in compose | Purpose | +| --- | --- | --- | +| `DATABASE_URL` | `postgres://postgres:postgres@postgres:5432/resource_manager_mqtt_ruby` | Rails and Solid Queue/Postgres storage | +| `MQTT_URL` | `mqtt://mosquitto:1883` | Broker consumed by the RM listener | +| `CEM_BASE_URL` | `ws://localhost:9999/s2` (standalone) / `ws://cem:9000/s2` (demo) | CEM WebSocket base URL; standalone the default is intentionally unreachable | +| `CEM_USERNAME`, `CEM_PASSWORD` | unset | Optional CEM WebSocket basic auth | +| `BASIC_AUTH_USERNAME`, `BASIC_AUTH_PASSWORD` | unset | Optional dashboard basic auth; off unless both are set | +| `SECRET_KEY_BASE` | set in compose | Rails secret key base (demo-only value in compose) | +| `S2_PROVIDES_FORECAST` | `true` | Enables the power-forecast translation path | +| `DEMO_ALLOW_FRAMING` | unset (standalone) / `1` (demo) | When `1`, drops `X-Frame-Options` so the demo screen can frame the dashboard, and enables the demo-only feed-reset endpoint; default-off keeps `SAMEORIGIN` and a strictly read-only dashboard | +| `IO_EVENT_SELECTOR` | `Select` | Forces the pure-Ruby event selector; the default epoll selector can end up spinning at 100% CPU after an S2 session teardown | + +## What is inside + +| Area | Files (under `rm/`) | +| --- | --- | +| MQTT → S2 translation | `app/lib/mqtt_message_handler.rb`, `app/lib/message_relayer.rb` | +| S2 message factories | `app/lib/frbc_system_description_factory.rb`, `app/lib/resource_manager_details_factory.rb` | +| S2 protocol handler | `app/lib/s2_message_handler.rb` | +| Connection lifecycle | `app/lib/supervisor.rb`, `app/lib/connection_registry.rb` | +| Reverse path (S2 → MQTT) | `app/jobs/process_frbc_instruction_job.rb` | +| Read-only bridge dashboard | `app/controllers/resources_controller.rb`, `app/controllers/traffic_controller.rb`, `app/lib/traffic_log.rb` | +| Bundled examples | `examples/*.json` | +| Single-source config | `app/lib/reference_config.rb` | + +The S2 protocol layer itself is the external `stekker/s2-ruby` gem and is not vendored here. The +`stekker-mqtt` contract gem is vendored under `rm/vendor/stekker-mqtt` so the build needs no private access. + +A spec suite lives under `rm/spec/` as a behavioral reference (notably the reverse-path job spec). It is +not part of the standalone demo path. + +## Conformance notes + +The bridge applies a few conformance workarounds required for interoperability with a CEM implementation. +They are interoperable (no fields the peer does not support): + +- The power commodity is hardcoded to `ELECTRIC.POWER.3_PHASE_SYMMETRIC` for every resource, including + single-phase ones (`app/lib/base_factory.rb`). +- The charging operation mode uses a near-zero fill rate (so the CEM controls based on power), and the storage + fill-level range is widened for headroom at 0% and 100% state of charge + (`app/lib/frbc_system_description_factory.rb`). + +These are interoperability choices, not standalone demo behavior. + +## Technology + +Rails 8, PostgreSQL 16, Eclipse Mosquitto 2, the [Async](https://github.com/socketry/async) reactor for +WebSocket I/O, Solid Queue/Cache/Cable (database-backed), and a small CSS design-token +stylesheet for the read-only dashboard. + diff --git a/resource-manager-mqtt-bridge-ruby/rm/.dockerignore b/resource-manager-mqtt-bridge-ruby/rm/.dockerignore new file mode 100644 index 0000000..85f1033 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/.dockerignore @@ -0,0 +1,14 @@ +/.git +/.bundle +/.env* +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +/tmp/pids/* +!/tmp/pids/.keep +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep +/public/assets diff --git a/resource-manager-mqtt-bridge-ruby/rm/.gitattributes b/resource-manager-mqtt-bridge-ruby/rm/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/resource-manager-mqtt-bridge-ruby/rm/.gitignore b/resource-manager-mqtt-bridge-ruby/rm/.gitignore new file mode 100644 index 0000000..8826cf5 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/.gitignore @@ -0,0 +1,17 @@ +/.bundle +/.env* +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep +/public/assets +/.rspec-failures +/docker-compose.override.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/.rspec b/resource-manager-mqtt-bridge-ruby/rm/.rspec new file mode 100644 index 0000000..a35c44f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/.rspec @@ -0,0 +1 @@ +--require rails_helper diff --git a/resource-manager-mqtt-bridge-ruby/rm/.ruby-version b/resource-manager-mqtt-bridge-ruby/rm/.ruby-version new file mode 100644 index 0000000..47b322c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/resource-manager-mqtt-bridge-ruby/rm/Dockerfile b/resource-manager-mqtt-bridge-ruby/rm/Dockerfile new file mode 100644 index 0000000..728ab2f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/Dockerfile @@ -0,0 +1,77 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This image is used by docker-compose.yml for the standalone reference demo. + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.1 +FROM ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development:test" + +# Build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libpq-dev libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY --link Gemfile Gemfile.lock .ruby-version ./ +COPY --link vendor/stekker-mqtt ./vendor/stekker-mqtt +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompile assets without requiring a real secret key at build time. +RUN SECRET_KEY_BASE_DUMMY=1 \ + DATABASE_URL=postgres://postgres:postgres@postgres:5432/resource_manager_mqtt_ruby \ + ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Install packages needed at runtime +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y \ + git \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the Rails server on the same internal port exposed by compose. +EXPOSE 3000 +CMD ["./bin/rails", "server", "-b", "0.0.0.0"] diff --git a/resource-manager-mqtt-bridge-ruby/rm/Gemfile b/resource-manager-mqtt-bridge-ruby/rm/Gemfile new file mode 100644 index 0000000..ced0782 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/Gemfile @@ -0,0 +1,37 @@ +source "https://rubygems.org" + +gem "async" +gem "async-http" +gem "async-websocket" +gem "bootsnap", require: false +gem "dry-struct" +gem "dry-types" +gem "ecs-logging" +gem "jbuilder" +gem "mqtt" +gem "pg", "~> 1.1" +gem "propshaft" +gem "puma", ">= 5.0" +gem "rails", "~> 8.1.2" +gem "s2-ruby", github: "stekker/s2-ruby", ref: "18583316a64e1d27e4273fe64a73988562848a6d", require: "s2" +gem "solid_cable" +gem "solid_cache" +gem "solid_queue" +gem "stekker-mqtt", path: "vendor/stekker-mqtt" +gem "tzinfo-data", platforms: [:windows, :jruby] + +group :development, :test do + gem "brakeman", require: false + gem "debug", platforms: [:mri, :windows] +end + +group :development do + gem "web-console" +end + +group :test do + gem "async-rspec" + gem "factory_bot_rails" + gem "rspec-rails" + gem "shoulda-matchers" +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock b/resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock new file mode 100644 index 0000000..f4cb144 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock @@ -0,0 +1,433 @@ +GIT + remote: https://github.com/stekker/s2-ruby.git + revision: 18583316a64e1d27e4273fe64a73988562848a6d + ref: 18583316a64e1d27e4273fe64a73988562848a6d + specs: + s2-ruby (0.1.0) + activesupport (>= 7.0) + async + async-websocket + dry-struct (~> 1.6) + dry-types (~> 1.7) + +PATH + remote: vendor/stekker-mqtt + specs: + stekker-mqtt (0.1.0) + activesupport (>= 6.0) + dry-struct + dry-types + mqtt + +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.18) + railties + actioncable (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + actionmailer (8.1.2.1) + actionpack (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2.1) + actionview (= 8.1.2.1) + activesupport (= 8.1.2.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.2.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.2.1) + activesupport (= 8.1.2.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.2.1) + activesupport (= 8.1.2.1) + globalid (>= 0.3.6) + activemodel (8.1.2.1) + activesupport (= 8.1.2.1) + activerecord (8.1.2.1) + activemodel (= 8.1.2.1) + activesupport (= 8.1.2.1) + timeout (>= 0.4.0) + activestorage (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activesupport (= 8.1.2.1) + marcel (~> 1.0) + activesupport (8.1.2.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + async (2.36.0) + console (~> 1.29) + fiber-annotation + io-event (~> 1.11) + metrics (~> 0.12) + traces (~> 0.18) + async-http (0.94.2) + async (>= 2.10.2) + async-pool (~> 0.11) + io-endpoint (~> 0.14) + io-stream (~> 0.6) + metrics (~> 0.12) + protocol-http (~> 0.58) + protocol-http1 (~> 0.36) + protocol-http2 (~> 0.22) + protocol-url (~> 0.2) + traces (~> 0.10) + async-pool (0.11.1) + async (>= 2.0) + async-rspec (1.17.1) + rspec (~> 3.0) + rspec-files (~> 1.0) + rspec-memory (~> 1.0) + async-websocket (0.30.0) + async-http (~> 0.76) + protocol-http (~> 0.34) + protocol-rack (~> 0.7) + protocol-websocket (~> 0.17) + base64 (0.3.0) + bigdecimal (4.1.0) + bindex (0.8.1) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.1.0) + racc + builder (3.3.0) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + console (1.34.2) + fiber-annotation + fiber-local (~> 1.1) + json + crass (1.0.6) + date (3.5.1) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.6.2) + drb (2.2.3) + dry-core (1.2.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.3.1) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-struct (1.8.0) + dry-core (~> 1.1) + dry-types (~> 1.8, >= 1.8.2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.9.0) + bigdecimal (>= 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + ecs-logging (1.0.0) + erb (6.0.4) + erubi (1.13.1) + et-orbi (1.3.0) + tzinfo + factory_bot (6.5.5) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.1) + fugit (1.11.2) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + io-console (0.8.2) + io-endpoint (0.17.2) + io-event (1.14.2) + io-stream (0.11.1) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.19.5) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + metrics (0.15.0) + mini_mime (1.1.5) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + mqtt (0.7.0) + logger + msgpack (1.8.0) + net-imap (0.6.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.3-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-musl) + racc (~> 1.4) + pg (1.6.2) + pg (1.6.2-aarch64-linux) + pg (1.6.2-aarch64-linux-musl) + pg (1.6.2-arm64-darwin) + pg (1.6.2-x86_64-linux) + pg (1.6.2-x86_64-linux-musl) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.2.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + protocol-hpack (1.5.1) + protocol-http (0.58.1) + protocol-http1 (0.37.0) + protocol-http (~> 0.58) + protocol-http2 (0.24.0) + protocol-hpack (~> 1.4) + protocol-http (~> 0.47) + protocol-rack (0.21.0) + io-stream (>= 0.10) + protocol-http (~> 0.58) + rack (>= 1.0) + protocol-url (0.4.0) + protocol-websocket (0.20.2) + protocol-http (~> 0.2) + psych (5.3.1) + date + stringio + puma (7.0.1) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.2.1) + actioncable (= 8.1.2.1) + actionmailbox (= 8.1.2.1) + actionmailer (= 8.1.2.1) + actionpack (= 8.1.2.1) + actiontext (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activemodel (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + bundler (>= 1.15.0) + railties (= 8.1.2.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + reline (0.6.3) + io-console (~> 0.5) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-files (1.1.3) + rspec (~> 3.0) + rspec-memory (1.0.4) + rspec (~> 3.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.5) + securerandom (0.4.1) + shoulda-matchers (6.5.0) + activesupport (>= 5.2.0) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.7) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.1) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (>= 1.3.1) + stringio (3.2.0) + thor (1.5.0) + timeout (0.6.1) + traces (0.18.2) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.5) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin-23 + arm64-darwin-24 + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + async + async-http + async-rspec + async-websocket + bootsnap + brakeman + debug + dry-struct + dry-types + ecs-logging + factory_bot_rails + jbuilder + mqtt + pg (~> 1.1) + propshaft + puma (>= 5.0) + rails (~> 8.1.2) + rspec-rails + s2-ruby! + shoulda-matchers + solid_cable + solid_cache + solid_queue + stekker-mqtt! + tzinfo-data + web-console + +BUNDLED WITH + 2.6.9 diff --git a/resource-manager-mqtt-bridge-ruby/rm/Rakefile b/resource-manager-mqtt-bridge-ruby/rm/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb new file mode 100644 index 0000000..1d2c1af --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb @@ -0,0 +1,27 @@ +class ApplicationController < ActionController::Base + before_action :authenticate_dashboard + after_action :disable_response_caching + + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + + private + + def disable_response_caching + response.headers["Cache-Control"] = "no-store" + end + + def authenticate_dashboard + return unless ReferenceConfig.dashboard_auth_enabled? + + authenticate_or_request_with_http_basic("S2 Resource Manager MQTT Bridge") do |username, password| + secure_compare(username, ReferenceConfig.basic_auth_username) && + secure_compare(password, ReferenceConfig.basic_auth_password) + end + end + + def secure_compare(left, right) + left.bytesize == right.bytesize && + ActiveSupport::SecurityUtils.secure_compare(left, right) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/concerns/.keep b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb new file mode 100644 index 0000000..da787e8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb @@ -0,0 +1,15 @@ +class ResourcesController < ApplicationController + def index + render :index, layout: "bridge" + end + + def show + resource = Resource.find(params[:id]) + connection = Supervisor.instance.find_connection(resource.s2_identifier) + + respond_to do |format| + format.html { render html: helpers.resource_uptime(connection) } + format.json { render json: connection&.state || {} } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb new file mode 100644 index 0000000..6f0af32 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb @@ -0,0 +1,32 @@ +class TrafficController < ApplicationController + skip_forgery_protection + + def reset + TrafficLog.clear + render json: { reset: true } + end + + def index + resource = Resource.order(:created_at).last + + render json: { + broker: ReferenceConfig.mqtt_url, + cem_endpoint: ReferenceConfig.cem.base_url, + supervisor: Supervisor.instance.status.to_s, + resource: resource && { + id: resource.s2_identifier, + state: connection_state(resource), + }, + traffic: TrafficLog.recent, + } + end + + private + + def connection_state(resource) + connection = Supervisor.instance.find_connection(resource.s2_identifier) + return connection.status.to_s if connection + + resource.disconnected_at ? "disconnected" : "idle" + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb b/resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb new file mode 100644 index 0000000..059aab8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb @@ -0,0 +1,7 @@ +module ApplicationHelper + def resource_uptime(connection) + return "N/A" unless connection&.connected_at + + distance_of_time_in_words(Time.current, connection.connected_at) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb new file mode 100644 index 0000000..e6ebb91 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb @@ -0,0 +1,66 @@ +class ProcessFRBCInstructionJob < ApplicationJob + def perform(resource_id:, frbc_system_description_json:, frbc_instruction_json:) + frbc_system_description = S2::Messages::FRBCSystemDescription.from_json!(frbc_system_description_json) + frbc_instruction = S2::Messages::FRBCInstruction.from_json!(frbc_instruction_json) + + mqtt_publisher.with_connection do |connection| + connection.publish( + topic: "stekker/resources/#{resource_id}/power_setpoints", + qos: 1, + message: build_power_setpoint_message(resource_id, frbc_system_description, frbc_instruction), + ) + + connection.publish( + topic: "stekker/resources/#{resource_id}/instruction_updates", + qos: 1, + message: build_instruction_update_message(resource_id, frbc_instruction), + ) + end + end + + private + + def mqtt_publisher + Stekker::MQTT::Publisher.new( + url: ReferenceConfig.mqtt_url, + logger: MQTTMessageLogger.new(Rails.configuration.ecs_logger), + ) + end + + def build_power_setpoint_message(resource_id, frbc_system_description, frbc_instruction) + starts_at = Time.zone.parse(frbc_instruction.execution_time) + ends_at = starts_at + (frbc_instruction.duration / 1000.0).seconds if frbc_instruction.duration.present? + + Stekker::MQTT::Messages::PowerSetpoint.from_dynamic!( + "message_type" => Stekker::MQTT::Messages::PowerSetpointMessageType::PowerSetpoint, + "resource_id" => resource_id, + "data" => { + "reference" => frbc_instruction.id, + "starts_at" => starts_at.iso8601, + "ends_at" => ends_at&.iso8601, + "unit" => "W", + "value" => calc_power_setpoint(frbc_system_description, frbc_instruction), + }, + ) + end + + def build_instruction_update_message(resource_id, frbc_instruction) + Stekker::MQTT::Messages::InstructionUpdate.from_dynamic!( + "message_type" => Stekker::MQTT::Messages::InstructionUpdateMessageType::InstructionUpdate, + "resource_id" => resource_id, + "data" => { + "reference" => frbc_instruction.id, + "updated_at" => Time.current.iso8601, + "status" => Stekker::MQTT::Messages::Status::Accepted, + }, + ) + end + + def calc_power_setpoint(frbc_system_description, frbc_instruction) # rubocop:disable Metrics/AbcSize + actuator = frbc_system_description.actuators.find { it.id == frbc_instruction.actuator_id } + operation_mode = actuator.operation_modes.find { it.id == frbc_instruction.operation_mode } + min_power = operation_mode.elements.first.power_ranges.first.start_of_range + max_power = operation_mode.elements.first.power_ranges.first.end_of_range + (((max_power - min_power) * frbc_instruction.operation_mode_factor) + min_power).round + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb new file mode 100644 index 0000000..56b4cef --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb @@ -0,0 +1,22 @@ +class PublishS2ReadyNotificationJob < ApplicationJob + def perform(resource_id:) + message = Stekker::MQTT::Messages::S2ReadyNotification.from_dynamic!( + "message_type" => Stekker::MQTT::Messages::S2ReadyNotificationMessageType::S2ReadyNotification, + "resource_id" => resource_id, + ) + + mqtt_publisher.publish( + topic: "stekker/resources/#{resource_id}/#{message.message_type.pluralize}", + message:, + ) + end + + private + + def mqtt_publisher + Stekker::MQTT::Publisher.new( + url: ReferenceConfig.mqtt_url, + logger: MQTTMessageLogger.new(Rails.configuration.ecs_logger), + ) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb new file mode 100644 index 0000000..878493b --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb @@ -0,0 +1,32 @@ +class SendHeartbeatPowerMeasurementJob < ApplicationJob + def perform(resource_id:) + mqtt_publisher.with_connection do |connection| + connection.publish( + topic: "stekker/resources/#{resource_id}/power_measurements", + qos: 0, + message: build_power_measurement(resource_id), + ) + end + end + + private + + def mqtt_publisher + Stekker::MQTT::Publisher.new( + url: ReferenceConfig.mqtt_url, + logger: MQTTMessageLogger.new(Rails.configuration.ecs_logger), + ) + end + + def build_power_measurement(resource_id) + Stekker::MQTT::Messages::PowerMeasurement.from_dynamic!( + "message_type" => Stekker::MQTT::Messages::PowerMeasurementMessageType::PowerMeasurement, + "resource_id" => resource_id, + "data" => { + "sampled_at" => Time.current.iso8601, + "unit" => Stekker::MQTT::Messages::Unit::W, + "value" => 0, + }, + ) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb new file mode 100644 index 0000000..676b9b3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb @@ -0,0 +1,9 @@ +class SendHeartbeatPowerMeasurementsJob < ApplicationJob + def perform + jobs = Resource.active.needing_heartbeat.map do |resource| + SendHeartbeatPowerMeasurementJob.new(resource_id: resource.s2_identifier) + end + + ActiveJob.perform_all_later(jobs) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb new file mode 100644 index 0000000..6c49f58 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb @@ -0,0 +1,10 @@ +class BaseFactory + class << self + # rubocop:disable Layout/LineLength + # Conformance workaround: hardcoded to 3_PHASE_SYMMETRIC for every resource, including single-phase resources, for CEM interoperability. + # rubocop:enable Layout/LineLength + def commodity_quantity_for(_resource) + S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb new file mode 100644 index 0000000..d6ccba2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb @@ -0,0 +1,34 @@ +class ConnectionRegistry + delegate :clear, + :each_value, + :find, + :size, + to: :@connections + + def initialize + @connections = {} + end + + def register_connection(resource_id, connection) + raise "Connection already registered for #{resource_id}" if registered_connection?(resource_id) + + @connections[resource_id] = connection + end + + def unregister_connection(resource_id) + @connections.delete(resource_id) + end + + def registered_connection?(resource_id) + @connections.has_key?(resource_id) + end + + def find_connection(resource_id, expected_status: nil) + connection = @connections[resource_id] + return nil unless connection + + return connection if expected_status.nil? || connection.status == expected_status + + nil + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb new file mode 100644 index 0000000..e83e42b --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb @@ -0,0 +1,10 @@ +class ErrorNotifier + class << self + def notify_error(error, extra: {}, tags: {}) + return if error.nil? + + Rails.logger.error("#{error.class}: #{error.message}") + Rails.logger.error(Array(error.backtrace).join("\n")) if Rails.env.local? + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb new file mode 100644 index 0000000..199bfaf --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb @@ -0,0 +1,125 @@ +class FRBCSystemDescriptionFactory < BaseFactory + OPERATION_MODE_OFF_LABEL = "Off".freeze + OPERATION_MODE_CHARGING_LABEL = "Charging".freeze + + # Workarounds to simplify EV control for CEM interoperability: + # - Extremely low fill_rate effectively disables the buffer, so the CEM bases control only on power + # - Extended fill_level_range (-10 to 110) provides headroom at SoC 0% and 100% + CEM_WORKAROUND_FILL_LEVEL_MIN = -10 + CEM_WORKAROUND_FILL_LEVEL_MAX = 110 + CEM_WORKAROUND_NONZERO_FILL_RATE = 0.00000001 + + class << self + def create(resource) # rubocop:disable Metrics/MethodLength + off_id = SecureRandom.uuid + charging_id = SecureRandom.uuid + commodity_quantity = commodity_quantity_for(resource) + minimum_power = calc_minimum_power(resource) + maximum_power = calc_maximum_power(resource) + + S2::Messages::FRBCSystemDescription.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::FRBCSystemDescriptionMessageType::FRBCSystemDescription, + "valid_from" => Time.current.iso8601, + "actuators" => [ + { + "id" => SecureRandom.uuid, + "diagnostic_label" => "Electric Vehicle Charger", + "supported_commodities" => [ + S2::Messages::Commodity::Electricity, + ], + "operation_modes" => [ + create_operation_mode( + id: off_id, + label: OPERATION_MODE_OFF_LABEL, + fill_level_range: { "start_of_range" => 0, "end_of_range" => 100 }, + fill_rate: { "start_of_range" => 0, "end_of_range" => 0 }, + power_ranges: [ + { + "start_of_range" => 0, + "end_of_range" => 0, + "commodity_quantity" => commodity_quantity, + }, + ], + ), + create_operation_mode( + id: charging_id, + label: OPERATION_MODE_CHARGING_LABEL, + fill_level_range: { "start_of_range" => 0, "end_of_range" => 100 }, + fill_rate: { "start_of_range" => 0.0, "end_of_range" => CEM_WORKAROUND_NONZERO_FILL_RATE }, + power_ranges: [ + { + "start_of_range" => minimum_power, + "end_of_range" => maximum_power, + "commodity_quantity" => commodity_quantity, + }, + ], + ), + ], + "transitions" => [ + create_transition(from: off_id, to: charging_id), + create_transition(from: charging_id, to: off_id), + ], + "timers" => [], + }, + ], + "storage" => { + "diagnostic_label" => "Electric Vehicle Battery", + "fill_level_label" => "State of Charge", + "provides_leakage_behaviour" => false, + "provides_fill_level_target_profile" => true, + "provides_usage_forecast" => false, + "fill_level_range" => { + "start_of_range" => CEM_WORKAROUND_FILL_LEVEL_MIN, + "end_of_range" => CEM_WORKAROUND_FILL_LEVEL_MAX, + }, + }, + ) + end + + private + + def create_operation_mode(id:, label:, fill_level_range:, fill_rate:, power_ranges:) + { + "id" => id, + "diagnostic_label" => label, + "elements" => [ + { + "fill_level_range" => fill_level_range, + "fill_rate" => fill_rate, + "power_ranges" => power_ranges, + }, + ], + "abnormal_condition_only" => false, + } + end + + def create_transition(from:, to:) + { + "id" => SecureRandom.uuid, + "from" => from, + "to" => to, + "start_timers" => [], + "blocking_timers" => [], + "transition_duration" => 3_000, + "abnormal_condition_only" => false, + } + end + + def calc_minimum_power(resource) + amperage = resource.configuration.fetch("minimum_current") + voltage = resource.configuration.fetch("voltage") + number_of_phases = resource.configuration.fetch("number_of_phases") + + amperage * voltage * number_of_phases + end + + def calc_maximum_power(resource) + amperage = resource.configuration.fetch("maximum_current") + voltage = resource.configuration.fetch("voltage") + number_of_phases = resource.configuration.fetch("number_of_phases") + + amperage * voltage * number_of_phases + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb new file mode 100644 index 0000000..82e70f3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb @@ -0,0 +1,15 @@ +class InstructionStatusTracker + def initialize + @relayed = Set.new + end + + def already_relayed?(resource_id:, instruction_id:, status:) + key = [resource_id, instruction_id, status] + + !@relayed.add?(key) + end + + def clear_resource(resource_id) + @relayed.delete_if { |key| key.first == resource_id } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb new file mode 100644 index 0000000..8aab9d5 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb @@ -0,0 +1,18 @@ +class MessageRelayer + def initialize(connection_registry) + @connection_registry = connection_registry + end + + def relay_message(resource_id:, message:) + expected_status = :connected + Rails.logger.info( + "[#{self.class}] Built S2 #{message.message_type} for resource_id #{resource_id}: #{message.to_json}", + ) + + if (connection = @connection_registry.find_connection(resource_id, expected_status:)) + connection.send_message(message) + else + Rails.logger.warn("[#{self.class}] No #{expected_status} connection found for resource_id #{resource_id}") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb new file mode 100644 index 0000000..a9366e1 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb @@ -0,0 +1,174 @@ +class MQTTMessageHandler < Stekker::MQTT::MessageHandler + MINIMUM_ELEMENT_DURATION = 15.minutes.freeze + + on Stekker::MQTT::Messages::Connect do |message| + configuration = message.data.to_dynamic + existing = Resource.find_by(s2_identifier: message.resource_id) + connection = Supervisor.instance.find_connection(message.resource_id) + already_live = existing && existing.disconnected_at.nil? && + existing.configuration == configuration && + connection&.status == :connected + + Resource.upsert( + { + s2_identifier: message.resource_id, + configuration: configuration, + disconnected_at: nil, + }, + unique_by: :s2_identifier, + ) + + Supervisor.instance.reconnect(message.resource_id) unless already_live + end + + on Stekker::MQTT::Messages::Disconnect do |message| + resource = Resource.find_by(s2_identifier: message.resource_id) + resource&.update!(disconnected_at: Time.current) + @instruction_status_tracker.clear_resource(message.resource_id) + Supervisor.instance.disconnect(message.resource_id) + end + + on Stekker::MQTT::Messages::PowerMeasurement do |message| + resource = Resource.find_by!(s2_identifier: message.resource_id) + + relay_message( + resource_id: message.resource_id, + message: S2::Messages::PowerMeasurement.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::PowerMeasurementMessageType::PowerMeasurement, + "measurement_timestamp" => message.data.sampled_at, + "values" => [ + { + "commodity_quantity" => BaseFactory.commodity_quantity_for(resource), + "value" => message.data.value, + }, + ], + ), + ) + + resource.record_power_measurement! + end + + on Stekker::MQTT::Messages::PowerForecast do |message| + next unless Rails.configuration.s2.provides_forecast + + resource = Resource.find_by!(s2_identifier: message.resource_id) + profile = message.data.values + start_at = Time.zone.parse(message.data.start_at) + values_timestamps = message.data.values.pluck(:timestamp).map { |ts| Time.zone.parse(ts) } + end_time = [start_at + 24.hours, *values_timestamps].max + + elements = profile.map.with_index do |power_forecast_value, index| + power_in_watts = power_forecast_value.value + next_element_start_time = Time.zone.parse(profile[index + 1].timestamp) if profile[index + 1].present? + element_end_time = next_element_start_time || end_time + element_start_time = Time.zone.parse(power_forecast_value.timestamp) + duration = (element_end_time - element_start_time).in_milliseconds.to_i + + { + "duration" => duration, + "power_values" => [ + { + "commodity_quantity" => BaseFactory.commodity_quantity_for(resource), + "value_expected" => power_in_watts, + }, + ], + } + end + + relay_message( + resource_id: message.resource_id, + message: S2::Messages::PowerForecast.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::PowerForecastMessageType::PowerForecast, + "start_time" => start_at.iso8601, + "elements" => elements, + ), + ) + end + + on Stekker::MQTT::Messages::ChargeStatus do |message| + relay_message( + resource_id: message.resource_id, + message: S2::Messages::FRBCStorageStatus.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::FRBCStorageStatusMessageType::FRBCStorageStatus, + "measurement_timestamp" => message.data.sampled_at, + "present_fill_level" => message.data.value, + ), + ) + end + + on Stekker::MQTT::Messages::ChargeTarget do |message| + starts_at = Time.zone.parse(message.data.starts_at) + ends_at = Time.zone.parse(message.data.ends_at) + main_element_duration = [ends_at - starts_at - MINIMUM_ELEMENT_DURATION, MINIMUM_ELEMENT_DURATION].max + + relay_message( + resource_id: message.resource_id, + message: S2::Messages::FRBCFillLevelTargetProfile.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::FRBCFillLevelTargetProfileMessageType::FRBCFillLevelTargetProfile, + "start_time" => message.data.starts_at, + "elements" => [ + { + "duration" => main_element_duration.in_milliseconds.to_i, + "fill_level_range" => { + "start_of_range" => message.data.direct_percentage, + "end_of_range" => message.data.smart_percentage, + }, + }, + { + "duration" => MINIMUM_ELEMENT_DURATION.in_milliseconds.to_i, + "fill_level_range" => { + "start_of_range" => message.data.smart_percentage, + "end_of_range" => message.data.smart_percentage, + }, + }, + ], + ), + ) + end + + on Stekker::MQTT::Messages::InstructionUpdate do |message| + if @instruction_status_tracker.already_relayed?( + resource_id: message.resource_id, + instruction_id: message.data.reference, + status: message.data.status, + ) + Rails.logger.info( + "[#{self.class}] Skipped duplicate instruction update " \ + "resource=#{message.resource_id} reference=#{message.data.reference} status=#{message.data.status}", + ) + next + end + + relay_message( + resource_id: message.resource_id, + message: S2::Messages::InstructionStatusUpdate.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::InstructionStatusUpdateMessageType::InstructionStatusUpdate, + "instruction_id" => message.data.reference, + "timestamp" => message.data.updated_at, + "status_type" => message.data.status, + ), + ) + end + + def initialize(message_relayer:, instruction_status_tracker: InstructionStatusTracker.new) + super() + + @message_relayer = message_relayer + @instruction_status_tracker = instruction_status_tracker + end + + def handle_message(payload) + Rails.application.executor.wrap { super } + end + + protected + + def relay_message(resource_id:, message:) + @message_relayer.relay_message(resource_id:, message:) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb new file mode 100644 index 0000000..09e726e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb @@ -0,0 +1,55 @@ +class MQTTMessageLogger + TOPIC_PATTERN = %r{\Astekker/resources/([^/]+)/} + + def initialize(logger) + @logger = logger + end + + def log_inbound(topic:, message:) + TrafficLog.record(peer: "mqtt", direction: "in", message_type: message_type_from(message), detail: detail_from(message)) + log("Inbound MQTT message", topic:, message:) + end + + def log_outbound(topic:, message:) + TrafficLog.record(peer: "mqtt", direction: "out", message_type: message_type_from(message), detail: detail_from(message)) + log("Outbound MQTT message", topic:, message:) + end + + private + + def message_type_from(message) + parsed = message.is_a?(String) ? JSON.parse(message) : message + parsed["message_type"] + rescue StandardError + nil + end + + def detail_from(message) + parsed = message.is_a?(String) ? JSON.parse(message) : message + data = parsed["data"] + return nil unless data.is_a?(Hash) && !data["value"].nil? + + data["unit"] ? "#{data['value']} #{data['unit']}" : data["value"].to_s + rescue StandardError + nil + end + + def log(log_message, topic:, message:) + @logger.info( + log_message, + mqtt: { + topic:, + resource_id: extract_resource_id(topic), + message:, + }.compact, + ) + rescue StandardError => e + ErrorNotifier.notify_error(e) + + raise if Rails.env.test? + end + + def extract_resource_id(topic) + topic.match(TOPIC_PATTERN)&.[](1) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb new file mode 100644 index 0000000..8946609 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb @@ -0,0 +1,44 @@ +class ReferenceConfig + Cem = Struct.new(:base_url, :username, :password, keyword_init: true) + + class << self + def mqtt_url + env_value("MQTT_URL") || "mqtt://mosquitto:1883" + end + + def cem + Cem.new( + base_url: env_value("CEM_BASE_URL") || "ws://localhost:9999/s2", + username: env_value("CEM_USERNAME"), + password: env_value("CEM_PASSWORD"), + ) + end + + def demo_allow_framing? + env_value("DEMO_ALLOW_FRAMING") == "1" + end + + def basic_auth_username + env_value("BASIC_AUTH_USERNAME") + end + + def basic_auth_password + env_value("BASIC_AUTH_PASSWORD") + end + + def dashboard_auth_enabled? + basic_auth_username && basic_auth_password + end + + def provides_forecast? + !%w[false 0 no off].include?((env_value("S2_PROVIDES_FORECAST") || "true").downcase) + end + + private + + def env_value(name) + value = ENV[name].to_s.strip + value.empty? ? nil : value + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb new file mode 100644 index 0000000..d53410a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb @@ -0,0 +1,32 @@ +class ResourceManagerDetailsFactory < BaseFactory + def self.create(resource) + S2::Messages::ResourceManagerDetails.from_dynamic!( + "manufacturer" => manufacturer_identifier, + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::ResourceManagerDetailsMessageType::ResourceManagerDetails, + "resource_id" => resource.s2_identifier, + "name" => resource.configuration.fetch("name"), + "roles" => [ + { + "commodity" => S2::Messages::Commodity::Electricity, + "role" => S2::Messages::RoleType::EnergyConsumer, + }, + ], + "instruction_processing_delay" => 5.minutes.in_milliseconds, + "available_control_types" => [ + S2::Messages::ControlType::FillRateBasedControl, + ], + "provides_forecast" => Rails.configuration.s2.provides_forecast, + "provides_power_measurement_types" => [ + commodity_quantity_for(resource), + ], + ) + end + + def self.manufacturer_identifier + git_rev = ENV.fetch("GIT_REV", "unknown")[0, 9] + s2_version = S2.supported_protocol_versions.first + + "StekkerS2/#{git_rev} (#{Rails.env}; Ruby/#{RUBY_VERSION}) S2/#{s2_version}" + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb new file mode 100644 index 0000000..3a9e45a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb @@ -0,0 +1,109 @@ +class S2MessageHandler < S2::MessageHandler + class ProtocolVersionMismatch < S2::MessageHandler::BaseError; end + + rescue_from ProtocolVersionMismatch do |error| + reply to: error.message_id, status: :permanent_error, diagnostic_label: error.message + end + + around_handle do |_payload, continue| + Rails.application.executor.wrap { continue.call } + end + + on S2::Messages::ReceptionStatus do |message| + @message_sender.receive_reception_status(message) + end + + on S2::Messages::Handshake do |message| + assert_state! message, status: :websocket_connected + assert_attribute! message, role: S2::Messages::EnergyManagementRole::Cem + + unless message.supported_protocol_versions.intersect?(S2.supported_protocol_versions) + raise( + ProtocolVersionMismatch.new( + "No protocol version matches, supported versions: #{S2.supported_protocol_versions.join(", ")}", + message.message_id, + ), + ) + end + + reply_ok to: message + end + + on S2::Messages::HandshakeResponse do |message| + assert_state! message, status: :websocket_connected + + reply_ok to: message + + update_state status: :initialized, + protocol_version: message.selected_protocol_version + + resource = Resource.find_by!(s2_identifier: @state[:resource_id]) + send_message ResourceManagerDetailsFactory.create(resource) + end + + on S2::Messages::SelectControlType do |message| + assert_state! message, status: :initialized + assert_attribute! message, control_type: S2::Messages::ControlType::FillRateBasedControl + + reply_ok to: message + + update_state control_type: message.control_type + + resource = Resource.find_by!(s2_identifier: @state[:resource_id]) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + send_message(frbc_system_description) do + update_state frbc_system_description: + end + + PublishS2ReadyNotificationJob.perform_later(resource_id: @state[:resource_id]) + end + + on S2::Messages::FRBCInstruction do |message| + assert_state! message, status: :initialized + assert_state! message, control_type: S2::Messages::ControlType::FillRateBasedControl + assert_actuator_id! message + assert_operation_mode_id! message + assert_duration! message + + reply_ok to: message + + ProcessFRBCInstructionJob.perform_later( + resource_id: @state[:resource_id], + frbc_system_description_json: @state[:frbc_system_description].to_json, + frbc_instruction_json: message.to_json, + ) + end + + private + + def assert_actuator_id!(frbc_instruction) + frbc_system_description = @state[:frbc_system_description] + return if frbc_system_description&.actuators&.any? { it.id == frbc_instruction.actuator_id } + + raise S2::MessageHandler::Asserting::InvalidContent.new( + "Invalid attribute: unknown actuator_id '#{frbc_instruction.actuator_id}'", + frbc_instruction.message_id, + ) + end + + def assert_operation_mode_id!(frbc_instruction) + frbc_system_description = @state[:frbc_system_description] + return if frbc_system_description&.actuators&.any? do |actuator| + actuator.operation_modes.any? { |mode| mode.id == frbc_instruction.operation_mode } + end + + raise S2::MessageHandler::Asserting::InvalidContent.new( + "Invalid attribute: unknown operation_mode '#{frbc_instruction.operation_mode}'", + frbc_instruction.message_id, + ) + end + + def assert_duration!(frbc_instruction) + return if frbc_instruction.duration.present? && frbc_instruction.duration.positive? + + raise S2::MessageHandler::Asserting::InvalidContent.new( + "Invalid attribute: duration must be present and positive, got '#{frbc_instruction.duration.inspect}'", + frbc_instruction.message_id, + ) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb new file mode 100644 index 0000000..bd1402a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb @@ -0,0 +1,41 @@ +class S2MessageLogger + def initialize(logger) + @logger = logger + end + + def log_inbound(resource_id:, message:) + message_type = extract_message_type(message) + TrafficLog.record(peer: "cem", direction: "in", message_type:) + log("Inbound S2 message", resource_id:, message:, message_type:) + end + + def log_outbound(resource_id:, message:) + message_type = extract_message_type(message) + TrafficLog.record(peer: "cem", direction: "out", message_type:) + log("Outbound S2 message", resource_id:, message:, message_type:) + end + + private + + def log(log_message, resource_id:, message:, message_type:) + @logger.info( + log_message, + s2: { + resource_id:, + message_type:, + message:, + }.compact, + ) + rescue StandardError => e + ErrorNotifier.notify_error(e) + + raise if Rails.env.test? + end + + def extract_message_type(message) + parsed = JSON.parse(message) + parsed["message_type"] + rescue JSON::ParserError + nil + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb new file mode 100644 index 0000000..61586f4 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb @@ -0,0 +1,33 @@ +class StekkerFormatter + def initialize(delegate = EcsLogging::Formatter.new) + @delegate = delegate + end + + def call(severity, time, progname, msg, **extras) + @delegate.call( + severity, + time, + progname, + msg, + **extras, + **metadata, + ) + end + + private + + def metadata + trace_id = SecureRandom.uuid + span_id = SecureRandom.uuid + + { + labels: { + environment: Rails.env, + git_sha: ENV.fetch("GIT_REV", "unknown"), + }, + "trace.id": trace_id, + "transaction.id": trace_id, + "span.id": span_id, + } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb new file mode 100644 index 0000000..c822796 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb @@ -0,0 +1,308 @@ +require "singleton" +require "async" + +class Supervisor + include Singleton + + INITIAL_RECONNECT_BACKOFF = 5.seconds + MAX_RECONNECT_BACKOFF = 5.minutes + + delegate :find_connection, to: :@connection_registry + + attr_reader :status + + def initialize + @connection_registry = ConnectionRegistry.new + @reactor_thread = nil + @reactor = nil + @reactor_mailbox = Queue.new # Ruby's thread-safe Queue + @wakeup_r = @wakeup_w = nil + @status = :initialized + + @reconnect_backoffs = {} + @scheduled_reconnects = Set.new + @connection_event_subscriptions = [] + + @mqtt_listener = nil + end + + def startup + return if reactor_running? + + Rails.logger.info "[#{self.class.name}] Starting reactor thread" + @status = :running + + subscribe_to_connection_events + + resource_ids = fetch_resource_ids + @reactor_thread = spawn_reactor_thread(resource_ids) + + start_mqtt_listener + end + + def shutdown # rubocop:disable Metrics/AbcSize + Rails.logger.info "[#{self.class.name}] Stopping #{@connection_registry.size} connections" + @status = :stopping + + stop_mqtt_listener + unsubscribe_from_connection_events + + if reactor_running? + post_to_reactor do + disconnect_all + Async::Task.current.reactor.stop + end + end + + stop_reactor_thread + @status = :stopped + Rails.logger.info "[#{self.class.name}] All disconnected" + end + + def connect(resource_id, parent: nil) + root_task = @reactor || parent + + return unless root_task + return if @connection_registry.registered_connection?(resource_id) + + credentials = ReferenceConfig.cem + ws_url = "#{credentials.base_url}/#{resource_id}" + headers = build_auth_headers(credentials) + + root_task.async do |task| + S2::Connection.new(resource_id:, task:, ws_url:, headers:).tap do |connection| + @connection_registry.register_connection(resource_id, connection) + connection.connect + end + end + end + + def disconnect(resource_id) + if (connection = @connection_registry.unregister_connection(resource_id)) + post_to_reactor { connection.disconnect } + else + Rails.logger.warn "[#{self.class.name}] Connection '#{resource_id}' not found" + end + end + + def reconnect(resource_id) + post_to_reactor do + if (connection = @connection_registry.unregister_connection(resource_id)) + connection.disconnect + end + + connect(resource_id) + end + end + + def send_message(resource_id, message) + with_connection(resource_id) do |connection| + connection.send_message(message) + end + end + + def running? + @status == :running + end + + private + + def subscribe_to_connection_events + @connection_event_subscriptions << ActiveSupport::Notifications.subscribe( + "connection_errored.session.s2", + ) do |event| + handle_connection_errored(event.payload[:resource_id]) + end + + @connection_event_subscriptions << ActiveSupport::Notifications.subscribe( + "connected.session.s2", + ) do |event| + @reconnect_backoffs.delete(event.payload[:resource_id]) + end + end + + def unsubscribe_from_connection_events + @connection_event_subscriptions.each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) } + @connection_event_subscriptions.clear + @scheduled_reconnects.clear + @reconnect_backoffs.clear + end + + def handle_connection_errored(resource_id) + return if resource_id.nil? + return unless @status == :running + return unless reactor_running? + return unless @connection_registry.registered_connection?(resource_id) + return unless @scheduled_reconnects.add?(resource_id) + + backoff = @reconnect_backoffs.fetch(resource_id, INITIAL_RECONNECT_BACKOFF) + @reconnect_backoffs[resource_id] = [backoff * 2, MAX_RECONNECT_BACKOFF].min + + Rails.logger.info "[#{self.class.name}] [#{resource_id}] Reconnecting in #{backoff.to_i}s" + spawn_reconnect(resource_id, backoff) + end + + def spawn_reconnect(resource_id, backoff) + post_to_reactor do + @reactor.async do + sleep backoff.to_f + + next unless @status == :running + next unless @connection_registry.registered_connection?(resource_id) + next if @connection_registry.find_connection(resource_id, expected_status: :connected) + + reconnect(resource_id) + rescue StandardError => e + ErrorNotifier.notify_error(e) + ensure + @scheduled_reconnects.delete(resource_id) + end + end + end + + def build_auth_headers(credentials) + return {} unless credentials.username && credentials.password + + token = Base64.strict_encode64("#{credentials.username}:#{credentials.password}") + { "authorization" => "Basic #{token}" } + end + + def with_connection(resource_id) + if (connection = find_connection(resource_id)) + yield connection + else + Rails.logger.warn "[#{self.class.name}] Connection '#{resource_id}' not found" + end + end + + def reactor_running? + @reactor_thread&.alive? + end + + def fetch_resource_ids + Resource.active.pluck(:s2_identifier) + end + + def spawn_reactor_thread(resource_ids) # rubocop:disable Metrics/AbcSize + Thread.new do # rubocop:disable Metrics/BlockLength + @wakeup_r, @wakeup_w = IO.pipe + + Async do |task| # rubocop:disable Metrics/BlockLength + @reactor = task + + task.async do + loop do + @wakeup_r.readpartial(1) + + loop do + job = begin + @reactor_mailbox.pop(true) # true = non-blocking + rescue ThreadError + break + end + + begin + job.call + rescue StandardError => e + ErrorNotifier.notify_error(e) + end + end + end + end + + resource_ids.each { |rid| connect(rid, parent: task) } + + begin + @wakeup_w.write(".") + rescue IOError + # ignore + end + + Async::Task.current.sleep + ensure + Rails.logger.info "[#{self.class.name}] Reactor thread exiting" + + @reactor = nil + @status = :stopped + + begin + @wakeup_r&.close + rescue IOError + # ignore + end + + begin + @wakeup_w&.close + rescue IOError + # ignore + end + + @wakeup_r = @wakeup_w = nil + end + end + end + + def run_reactor(task, resource_ids) + @reactor = task + + Rails.logger.info "[#{self.class.name}] Connecting to resources #{resource_ids}" + resource_ids.each { |resource_id| connect(resource_id, parent: task) } + + Async::Task.current.sleep + end + + def disconnect_all + @connection_registry.each_value(&:disconnect) + @connection_registry.clear + end + + def stop_reactor_thread + return unless @reactor_thread + + @reactor_thread.join + @reactor_thread = nil + end + + def post_to_reactor(&block) + return unless reactor_running? + + @reactor_mailbox << block + + begin + @wakeup_w&.write(".") + rescue IOError + # ignore + end + end + + def start_mqtt_listener + sleep 1 unless Rails.env.test? # wait a bit to let S2 connections establish first + + @mqtt_listener = Stekker::MQTT::Listener.new( + url: ReferenceConfig.mqtt_url, + connect_opts: { + client_id: "stekker_s2", + clean_session: false, + }, + message_handler: MQTTMessageHandler.new( + message_relayer: MessageRelayer.new(@connection_registry), + ), + executor: Rails.application.reloader, + topics: { + "stekker/resources/+/charge_statuses" => 0, + "stekker/resources/+/charge_targets" => 0, + "stekker/resources/+/connects" => 0, + "stekker/resources/+/disconnects" => 0, + "stekker/resources/+/instruction_updates" => 1, + "stekker/resources/+/power_forecasts" => 0, + "stekker/resources/+/power_measurements" => 0, + }, + ) + @mqtt_listener.start + end + + def stop_mqtt_listener + @mqtt_listener&.stop + @mqtt_listener = nil + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb b/resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb new file mode 100644 index 0000000..02baefd --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb @@ -0,0 +1,48 @@ +class TrafficLog + MAX = 80 + PATH = Rails.root.join("tmp", "traffic.jsonl") + + class << self + def record(peer:, direction:, message_type:, detail: nil) + return if message_type.blank? + + entry = JSON.generate({ peer: peer, direction: direction, message_type: message_type, detail: detail, at: Time.current.utc.iso8601 }.compact) + File.open(PATH, File::RDWR | File::CREAT, 0o644) do |file| + file.flock(File::LOCK_EX) + kept = file.read.split("\n").last(MAX - 1) + kept << entry + file.rewind + file.truncate(0) + file.write(kept.join("\n") << "\n") + end + rescue StandardError => e + Rails.logger.warn("TrafficLog.record failed: #{e.message}") + nil + end + + def clear + File.open(PATH, File::RDWR | File::CREAT, 0o644) do |file| + file.flock(File::LOCK_EX) + file.truncate(0) + end + rescue StandardError => e + Rails.logger.warn("TrafficLog.clear failed: #{e.message}") + nil + end + + def recent + return [] unless File.exist?(PATH) + + lines = File.open(PATH, File::RDONLY) do |file| + file.flock(File::LOCK_SH) + file.read.split("\n") + end + + lines.reverse.filter_map do |line| + JSON.parse(line) + rescue JSON::ParserError + nil + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb b/resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/concerns/.keep b/resource-manager-mqtt-bridge-ruby/rm/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb b/resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb new file mode 100644 index 0000000..e488f19 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb @@ -0,0 +1,16 @@ +class Resource < ApplicationRecord + ALMOST_15_MINUTES = 888.seconds + + validates :s2_identifier, presence: true, uniqueness: true + + scope :active, -> { where(disconnected_at: nil) } + scope :needing_heartbeat, -> { where(last_power_measurement_at: [nil, ...ALMOST_15_MINUTES.ago]) } + + def record_power_measurement! + update!(last_power_measurement_at: Time.current) + end + + def active? + disconnected_at.blank? + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb new file mode 100644 index 0000000..3754a12 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb @@ -0,0 +1,56 @@ +class LogMQTTListenerSubscriber < ActiveSupport::Subscriber + attach_to :"listener.mqtt.stekker" + + def starting(_event) + Rails.logger.info("[#{self.class.name}] Starting MQTT listener") + end + + def connected(_event) + Rails.logger.info("[#{self.class.name}] MQTT listener connected") + end + + def subscribed(event) + topics = event.payload[:topics] + Rails.logger.info("[#{self.class.name}] Subscribed to MQTT topics: #{topics}") + end + + def message_received(event) + topic = event.payload[:topic] + message = event.payload[:message] + + mqtt_logger.log_inbound(topic:, message:) + end + + def listen_errored(event) + Rails.logger.error("[#{self.class.name}] MQTT listener error") + ErrorNotifier.notify_error(event.payload[:exception]) + end + + def connection_errored(event) + Rails.logger.error("[#{self.class.name}] MQTT connection error") + ErrorNotifier.notify_error(event.payload[:exception]) + end + + def get_errored(event) + Rails.logger.error("[#{self.class.name}] MQTT get error") + ErrorNotifier.notify_error(event.payload[:exception]) + end + + def graceful_shutdown(_event) + Rails.logger.info("[#{self.class.name}] MQTT listener shutting down gracefully") + end + + def thread_exited(_event) + Rails.logger.info("[#{self.class.name}] MQTT listener thread exited") + end + + def stopped(_event) + Rails.logger.info("[#{self.class.name}] MQTT listener stopped") + end + + private + + def mqtt_logger + MQTTMessageLogger.new(Rails.configuration.ecs_logger) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb new file mode 100644 index 0000000..25c31cd --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb @@ -0,0 +1,8 @@ +class LogMQTTMessageHandlerSubscriber < ActiveSupport::Subscriber + attach_to :"message_handler.mqtt.stekker" + + def message_errored(event) + Rails.logger.error("[#{self.class.name}] MQTT message handling error") + ErrorNotifier.notify_error(event.payload[:exception]) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb new file mode 100644 index 0000000..251c5ff --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb @@ -0,0 +1,67 @@ +class LogSessionSubscriber < ActiveSupport::Subscriber + attach_to :"session.s2" + + def connected(event) + Rails.logger.info( + "[#{self.class.name}] [#{event.payload[:resource_id]}] S2 Session connected to #{event.payload[:url]}", + ) + end + + def started(event) + Rails.logger.info("[#{self.class.name}] [#{event.payload[:resource_id]}] S2 Session started") + end + + def disconnected(event) + Rails.logger.info("[#{self.class.name}] [#{event.payload[:resource_id]}] S2 Session disconnected") + end + + def stopped(event) + Rails.logger.info("[#{self.class.name}] [#{event.payload[:resource_id]}] S2 Session stopped") + end + + def message_received(event) + payload = event.payload[:payload] + resource_id = event.payload[:resource_id] + + s2_logger.log_inbound(resource_id:, message: payload) + end + + def message_sent(event) + payload = event.payload[:payload] + resource_id = event.payload[:resource_id] + + s2_logger.log_outbound(resource_id:, message: payload) + end + + def connection_errored(event) + exception = event.payload[:exception] + resource_id = event.payload[:resource_id] + + ErrorNotifier.notify_error( + exception, + extra: response_details(exception), + tags: { resource_id: resource_id }.compact, + ) + end + + def message_errored(event) + ErrorNotifier.notify_error(event.payload[:exception]) + end + + private + + def s2_logger + S2MessageLogger.new(Rails.configuration.ecs_logger) + end + + def response_details(exception) + return {} unless exception.is_a?(Async::WebSocket::ConnectionError) && exception.response + + response = exception.response + { + http_status: response.status, + http_headers: response.headers.to_h.transform_values { |value| Array(value).join(", ") }, + http_body: response.body&.read&.byteslice(0, 1024), + }.compact + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb b/resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb new file mode 100644 index 0000000..dec3ee0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb @@ -0,0 +1,12 @@ + + + + + Resource Manager (bridge) + + + + + <%= yield %> + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb b/resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb new file mode 100644 index 0000000..943418e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb @@ -0,0 +1,133 @@ +
+
+
S2 RM to MQTT Bridgethe reference implementation
+
Speaks MQTT to the charger and S2 to the CEM, and translates between them both ways.
+
+ +
+
+
+ + +
No messages yet.
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/bundle b/resource-manager-mqtt-bridge-ruby/rm/bin/bundle new file mode 100755 index 0000000..c4f9ab9 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/bin/bundle @@ -0,0 +1,113 @@ +#!/usr/bin/env ruby +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) + end + + def env_var_version + ENV.fetch("BUNDLER_VERSION", nil) + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + + bundler_version = Regexp.last_match(1) + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV.fetch("BUNDLE_GEMFILE", nil) + return gemfile if gemfile + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + + require_error = activation_error_handling do + require "bundler/version" + end + if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + return + end + + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path("bundler", "bundle") if m.invoked_as_script? diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint b/resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint new file mode 100755 index 0000000..237dd12 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate the database. +if [ "$1" == "./bin/rails" ] && [ "$2" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/jobs b/resource-manager-mqtt-bridge-ruby/rm/bin/jobs new file mode 100755 index 0000000..dcf59f3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/rails b/resource-manager-mqtt-bridge-ruby/rm/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/rake b/resource-manager-mqtt-bridge-ruby/rm/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/resource-manager-mqtt-bridge-ruby/rm/config.ru b/resource-manager-mqtt-bridge-ruby/rm/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/application.rb b/resource-manager-mqtt-bridge-ruby/rm/config/application.rb new file mode 100644 index 0000000..8d930c3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/application.rb @@ -0,0 +1,50 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +require "action_cable/engine" +# require "rails/test_unit/railtie" + +require_relative "../app/lib/reference_config" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module StekkerS2 + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + config.active_support.isolation_level = :fiber + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil + + config.s2 = ActiveSupport::OrderedOptions.new + config.s2.provides_forecast = ReferenceConfig.provides_forecast? + + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/boot.rb b/resource-manager-mqtt-bridge-ruby/rm/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/cable.yml b/resource-manager-mqtt-bridge-ruby/rm/config/cable.yml new file mode 100644 index 0000000..4f8054a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/cable.yml @@ -0,0 +1,14 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/cache.yml b/resource-manager-mqtt-bridge-ruby/rm/config/cache.yml new file mode 100644 index 0000000..ae55ceb --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/cache.yml @@ -0,0 +1,15 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/database.yml b/resource-manager-mqtt-bridge-ruby/rm/config/database.yml new file mode 100644 index 0000000..2aeee5a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/database.yml @@ -0,0 +1,87 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + + +development: + <<: *default + database: stekker_s2_development + + # The specified database role being used to connect to PostgreSQL. + # To create additional roles in PostgreSQL see `$ createuser --help`. + # When left blank, PostgreSQL will use the default role. This is + # the same name as the operating system user running Rails. + #username: stekker_s2 + + # The password associated with the PostgreSQL role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + username: <%= ENV.fetch('STEKKER_DATABASE_USERNAME', 'stekker_s2') %> + database: stekker_s2_test<%= ENV['TEST_ENV_NUMBER'] %> + password: <%= ENV.fetch('STEKKER_DATABASE_PASSWORD', 'stekker_s2') %> + host: 127.0.0.1 + pool: 5 + timeout: 2000 + +# Provide the production connection URL through the environment when you boot +# the app. The standalone compose file sets this for the demo: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# + +production: + <<: *default +<% unless ENV["DATABASE_URL"].to_s.empty? %> + url: <%= ENV.fetch("DATABASE_URL") %> +<% end %> diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environment.rb b/resource-manager-mqtt-bridge-ruby/rm/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb b/resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb new file mode 100644 index 0000000..1da9f37 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb @@ -0,0 +1,71 @@ +require "active_support/core_ext/integer/time" +require "ecs_logging/logger" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! + + # ECS-formatted logging (null logger for development) + config.ecs_logger = EcsLogging::Logger.new(IO::NULL) + +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb b/resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb new file mode 100644 index 0000000..f93363c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb @@ -0,0 +1,84 @@ +require "active_support/core_ext/integer/time" +require "ecs_logging/logger" +require_relative "../../app/lib/stekker_formatter" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + config.public_file_server.headers = { "cache-control" => "no-store" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + config.log_tags = [:request_id] + config.logger = ActiveSupport::TaggedLogging.logger($stdout) + + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [:id] + + # ECS-formatted logs are written to stdout for `docker compose logs app`. + config.ecs_logger = EcsLogging::Logger.new($stdout).tap do |lgr| + lgr.formatter = StekkerFormatter.new + end + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + + if ENV["DEMO_ALLOW_FRAMING"] == "1" + config.action_dispatch.default_headers = { + "X-Content-Type-Options" => "nosniff", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin", + "Content-Security-Policy" => "frame-ancestors 'self' http://localhost:8080", + } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb b/resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb new file mode 100644 index 0000000..5e395bc --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb @@ -0,0 +1,54 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +require "ecs_logging/logger" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before running the app. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + if config.respond_to?(:factory_bot) + config.factory_bot.definition_file_paths << Stekker::MQTT::Factories::LOAD_PATH + end + + Stekker::MQTT::MessageHandler.raise_handling_errors = true + + # ECS-formatted logging (null logger for tests) + config.ecs_logger = EcsLogging::Logger.new(IO::NULL) + +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb new file mode 100644 index 0000000..4873244 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..d51d713 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c0b717f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb new file mode 100644 index 0000000..c0e8139 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym "FRBC" + inflect.acronym "MQTT" +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb new file mode 100644 index 0000000..4451d8c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb @@ -0,0 +1,10 @@ +Rails.application.config.to_prepare do + { + "session.s2": LogSessionSubscriber, + "listener.mqtt.stekker": LogMQTTListenerSubscriber, + "message_handler.mqtt.stekker": LogMQTTMessageHandlerSubscriber, + }.each do |event, subscriber| + subscriber.detach_from event + subscriber.attach_to event + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb new file mode 100644 index 0000000..4145417 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb @@ -0,0 +1,75 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 8.1 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `8.1`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Skips escaping HTML entities and line separators. When set to `false`, the +# JSON renderer no longer escapes these to improve performance. +# +# Example: +# class PostsController < ApplicationController +# def index +# render json: { key: "\u2028\u2029<>&" } +# end +# end +# +# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"

<>&"}` +# with the config set to `false`. +# +# Applications that want to keep the escaping behavior can set the config to `true`. +#++ +# Rails.configuration.action_controller.escape_json_responses = false + +### +# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON. +# +# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript +# 2019. As such it's no longer a concern in modern browsers: +# https://caniuse.com/mdn-javascript_builtins_json_json_superset. +#++ +# Rails.configuration.active_support.escape_js_separators_in_json = false + +### +# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values +# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or +# `primary_key`) to fall back on. +# +# The current behavior of not raising an error has been deprecated, and this configuration option will be removed +# in Rails 8.2. +#++ +# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true + +### +# Controls how Rails handles path relative URL redirects. +# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError` +# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities. +# +# Example: +# redirect_to "example.com" # Raises UnsafeRedirectError +# redirect_to "@attacker.com" # Raises UnsafeRedirectError +# redirect_to "/safe/path" # Works correctly +# +# Applications that want to allow these redirects can set the config to `:log` (previous default) +# to only log warnings, or `:notify` to send ActiveSupport notifications. +#++ +# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise + +### +# Use a Ruby parser to track dependencies between Action View templates +#++ +# Rails.configuration.action_view.render_tracker = :ruby + +### +# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields +# included in `button_to` forms will omit the `autocomplete="off"` attribute. +# +# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`. +#++ +# Rails.configuration.action_view.remove_hidden_field_autocomplete = true diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb new file mode 100644 index 0000000..831014d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb @@ -0,0 +1,20 @@ +Rails.application.config.to_prepare do + S2.logger = Rails.logger + S2.message_handler_class = S2MessageHandler +end + +if defined?(Rails::Server) + Rails.logger.info "[Initializer] Rails server starting, initializing Supervisor" + + Rails.application.config.after_initialize do + Rails.logger.info "[Initializer] Starting Supervisor" + Supervisor.instance.startup + end + + at_exit do + Rails.logger.info "[Initializer] Stopping Supervisor" + Supervisor.instance.shutdown + end +else + Rails.logger.info "[Initializer] Not a Rails server, skipping Supervisor initialization" +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/puma.rb b/resource-manager-mqtt-bridge-ruby/rm/config/puma.rb new file mode 100644 index 0000000..0bd1efc --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside Puma for this single-server demo. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/queue.yml b/resource-manager-mqtt-bridge-ruby/rm/config/queue.yml new file mode 100644 index 0000000..9eace59 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml b/resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml new file mode 100644 index 0000000..d7f1133 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml @@ -0,0 +1,18 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 + send_heartbeat_power_measurements: + class: SendHeartbeatPowerMeasurementsJob + schedule: "0,15,30,45 * * * *" diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/routes.rb b/resource-manager-mqtt-bridge-ruby/rm/config/routes.rb new file mode 100644 index 0000000..325d373 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/config/routes.rb @@ -0,0 +1,10 @@ +Rails.application.routes.draw do + get "up" => "rails/health#show", as: :rails_health_check + + root "resources#index" + + get "traffic", to: "traffic#index" + post "traffic/reset", to: "traffic#reset" if ReferenceConfig.demo_allow_framing? + + resources :resources, only: [:index, :show] +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb new file mode 100644 index 0000000..218ab49 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb @@ -0,0 +1,10 @@ +class CreateResources < ActiveRecord::Migration[8.0] + def change + create_table :resources do |t| + t.string :s2_identifier, null: false + t.string :stekker_identifier, null: false + + t.timestamps + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb new file mode 100644 index 0000000..b942372 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb @@ -0,0 +1,13 @@ +class SetupSolidCable < ActiveRecord::Migration[8.0] + def change + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536_870_912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb new file mode 100644 index 0000000..b0f9813 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb @@ -0,0 +1,135 @@ +class SetupSolidQueue < ActiveRecord::Migration[8.0] + def change + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index %w[concurrency_key priority job_id], name: "index_solid_queue_blocked_executions_for_release" + t.index %w[expires_at concurrency_key], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index %w[process_id job_id], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index %w[queue_name finished_at], name: "index_solid_queue_jobs_for_filtering" + t.index %w[scheduled_at finished_at], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index %w[name supervisor_id], + name: "index_solid_queue_processes_on_name_and_supervisor_id", + unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index %w[priority job_id], name: "index_solid_queue_poll_all" + t.index %w[queue_name priority job_id], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index %w[task_key run_at], + name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", + unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index %w[scheduled_at priority job_id], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index %w[key value], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb new file mode 100644 index 0000000..9bbce96 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb @@ -0,0 +1,14 @@ +class SetupSolidCache < ActiveRecord::Migration[8.0] + def change + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536_870_912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index %w[key_hash byte_size], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb new file mode 100644 index 0000000..2746c57 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb @@ -0,0 +1,5 @@ +class RemoveStekkerIdentifierFromResources < ActiveRecord::Migration[8.0] + def change + remove_column :resources, :stekker_identifier, :string, null: false + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb new file mode 100644 index 0000000..d5cf7dc --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb @@ -0,0 +1,5 @@ +class AddConfigurationToResource < ActiveRecord::Migration[8.0] + def change + add_column :resources, :configuration, :jsonb, null: false, default: {} + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb new file mode 100644 index 0000000..b7fceee --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb @@ -0,0 +1,5 @@ +class AddUniquenessToResourceS2Identifier < ActiveRecord::Migration[8.0] + def change + add_index :resources, :s2_identifier, unique: true + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb new file mode 100644 index 0000000..18525dc --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb @@ -0,0 +1,5 @@ +class AddDisconnectedAtToResource < ActiveRecord::Migration[8.0] + def change + add_column :resources, :disconnected_at, :datetime + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb new file mode 100644 index 0000000..4a0785b --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb @@ -0,0 +1,5 @@ +class AddLastPowerMeasurementAtToResources < ActiveRecord::Migration[8.1] + def change + add_column :resources, :last_power_measurement_at, :datetime + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/schema.rb b/resource-manager-mqtt-bridge-ruby/rm/db/schema.rb new file mode 100644 index 0000000..dbb0106 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/db/schema.rb @@ -0,0 +1,175 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2026_03_03_155137) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + + create_table "resources", force: :cascade do |t| + t.jsonb "configuration", default: {}, null: false + t.datetime "created_at", null: false + t.datetime "disconnected_at" + t.datetime "last_power_measurement_at" + t.string "s2_identifier", null: false + t.datetime "updated_at", null: false + t.index ["s2_identifier"], name: "index_resources_on_s2_identifier", unique: true + end + + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", null: false + t.bigint "channel_hash", null: false + t.datetime "created_at", null: false + t.binary "payload", null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end + + create_table "solid_cache_entries", force: :cascade do |t| + t.integer "byte_size", null: false + t.datetime "created_at", null: false + t.binary "key", null: false + t.bigint "key_hash", null: false + t.binary "value", null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end + + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.string "concurrency_key", null: false + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.bigint "process_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "error" + t.bigint "job_id", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "active_job_id" + t.text "arguments" + t.string "class_name", null: false + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "finished_at" + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at" + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "queue_name", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "hostname" + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.text "metadata" + t.string "name", null: false + t.integer "pid", null: false + t.bigint "supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.datetime "run_at", null: false + t.string "task_key", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.text "arguments" + t.string "class_name" + t.string "command", limit: 2048 + t.datetime "created_at", null: false + t.text "description" + t.string "key", null: false + t.integer "priority", default: 0 + t.string "queue_name" + t.string "schedule", null: false + t.boolean "static", default: true, null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.string "key", null: false + t.datetime "updated_at", null: false + t.integer "value", default: 1, null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/seeds.rb b/resource-manager-mqtt-bridge-ruby/rm/db/seeds.rb new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml b/resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml new file mode 100644 index 0000000..5b2a023 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml @@ -0,0 +1,47 @@ +name: resource-manager-mqtt-ruby + +services: + app: + build: . + depends_on: + postgres: + condition: service_healthy + mosquitto: + condition: service_started + environment: + RAILS_ENV: production + RAILS_LOG_LEVEL: info + SECRET_KEY_BASE: "demo-secret-key-base-for-standalone-reference-only" + DATABASE_URL: "postgres://postgres:postgres@postgres:5432/resource_manager_mqtt_ruby" + MQTT_URL: "mqtt://mosquitto:1883" + CEM_BASE_URL: "ws://localhost:9999/s2" + S2_PROVIDES_FORECAST: "true" + SOLID_QUEUE_IN_PUMA: "true" + IO_EVENT_SELECTOR: "Select" # the default epoll selector can spin at 100% CPU after an S2 session teardown; the pure-Ruby selector is immune + ports: + - "3000:3000" + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: resource_manager_mqtt_ruby + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d resource_manager_mqtt_ruby"] + interval: 2s + timeout: 5s + retries: 20 + volumes: + - postgres-data:/var/lib/postgresql/data + + mosquitto: + image: eclipse-mosquitto:2 + ports: + - "1883:1883" + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - ./examples:/examples:ro + +volumes: + postgres-data: diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json b/resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json new file mode 100644 index 0000000..668a0d2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json @@ -0,0 +1,8 @@ +{ + "message_type": "charge_status", + "resource_id": "demo-resource-001", + "data": { + "sampled_at": "2026-06-04T10:05:00Z", + "value": 42.5 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json b/resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json new file mode 100644 index 0000000..4a1ff72 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json @@ -0,0 +1,10 @@ +{ + "message_type": "charge_target", + "resource_id": "demo-resource-001", + "data": { + "starts_at": "2026-06-04T10:00:00Z", + "ends_at": "2026-06-04T18:00:00Z", + "direct_percentage": 25.0, + "smart_percentage": 85.0 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/connect.json b/resource-manager-mqtt-bridge-ruby/rm/examples/connect.json new file mode 100644 index 0000000..1d2fde9 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/connect.json @@ -0,0 +1,11 @@ +{ + "message_type": "connect", + "resource_id": "demo-resource-001", + "data": { + "name": "Demo EV charger", + "voltage": 230, + "number_of_phases": 3, + "minimum_current": 0, + "maximum_current": 16 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json b/resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json new file mode 100644 index 0000000..993af8d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json @@ -0,0 +1,4 @@ +{ + "message_type": "disconnect", + "resource_id": "demo-resource-001" +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json b/resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json new file mode 100644 index 0000000..12f78e0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json @@ -0,0 +1,9 @@ +{ + "message_type": "instruction_update", + "resource_id": "demo-resource-001", + "data": { + "reference": "demo-instruction-001", + "updated_at": "2026-06-04T10:10:00Z", + "status": "SUCCEEDED" + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json b/resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json new file mode 100644 index 0000000..c7f35d0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json @@ -0,0 +1,22 @@ +{ + "message_type": "power_forecast", + "resource_id": "demo-resource-001", + "data": { + "start_at": "2026-06-04T10:00:00Z", + "unit": "W", + "values": [ + { + "timestamp": "2026-06-04T10:00:00Z", + "value": 3700 + }, + { + "timestamp": "2026-06-04T10:15:00Z", + "value": 7400 + }, + { + "timestamp": "2026-06-04T10:45:00Z", + "value": 0 + } + ] + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json b/resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json new file mode 100644 index 0000000..ea28589 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json @@ -0,0 +1,9 @@ +{ + "message_type": "power_measurement", + "resource_id": "demo-resource-001", + "data": { + "sampled_at": "2026-06-04T10:00:00Z", + "unit": "W", + "value": 3700 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/lib/tasks/.keep b/resource-manager-mqtt-bridge-ruby/rm/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/log/.keep b/resource-manager-mqtt-bridge-ruby/rm/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf b/resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf new file mode 100644 index 0000000..c8348ac --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 +allow_anonymous true diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/400.html b/resource-manager-mqtt-bridge-ruby/rm/public/400.html new file mode 100644 index 0000000..ced6482 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/400.html @@ -0,0 +1,120 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/404.html b/resource-manager-mqtt-bridge-ruby/rm/public/404.html new file mode 100644 index 0000000..16ec3ce --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/404.html @@ -0,0 +1,120 @@ + + + + + + + The page you were looking for doesn't exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html b/resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html new file mode 100644 index 0000000..765064c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html @@ -0,0 +1,120 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/422.html b/resource-manager-mqtt-bridge-ruby/rm/public/422.html new file mode 100644 index 0000000..56b9409 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/422.html @@ -0,0 +1,120 @@ + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/500.html b/resource-manager-mqtt-bridge-ruby/rm/public/500.html new file mode 100644 index 0000000..ef5ea47 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/500.html @@ -0,0 +1,120 @@ + + + + + + + We're sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We're sorry, but something went wrong.
If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/icon.png b/resource-manager-mqtt-bridge-ruby/rm/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9b1592ef98a9fbfa863094d01522563ac29e3db2 GIT binary patch literal 30367 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 + + + + + + + + + + + diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/robots.txt b/resource-manager-mqtt-bridge-ruby/rm/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/tokens.css b/resource-manager-mqtt-bridge-ruby/rm/public/tokens.css new file mode 100644 index 0000000..4fc9565 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/public/tokens.css @@ -0,0 +1,174 @@ +:root { + --bg: #eef1f5; + --panel: #ffffff; + --panel-2: #f3f6f9; + --border: #dce2e9; + --border-strong: #c5cdd7; + --text: #1b2733; + --muted: #51606f; + --faint: #6b7685; + + --brand: #0d8f61; + --brand-ink: #0a7a52; + --brand-deep: #08643f; + --brand-soft: #e6f6ef; + + --report: #2563b0; + --report-ink: #1f4f8f; + --report-soft: #e8effb; + + --command: #a85c0a; + --command-ink: #8a4c08; + --command-soft: #fbf0df; + + --setup: #6b7685; + + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --radius: 10px; + --radius-sm: 7px; + --gap: 14px; + --header-min: 84px; + --primary-min: 76px; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 14px; + -webkit-font-smoothing: antialiased; +} + +.panel { + display: flex; + flex-direction: column; + height: 100vh; + padding: var(--gap); + gap: var(--gap); + background: var(--bg); +} + +.panel-header { + display: flex; + flex-direction: column; + gap: 4px; + min-height: var(--header-min); + padding-bottom: var(--gap); + border-bottom: 1px solid var(--border); +} + +.primary { + display: flex; + flex-direction: column; + gap: 10px; + min-height: var(--primary-min); +} + +.panel-title { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.1px; + color: var(--text); +} + +.panel-role { + font-size: 12.5px; + line-height: 1.45; + color: var(--muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.panel-tag { + margin-left: 9px; + font-size: 12px; + font-weight: 550; + letter-spacing: 0; + color: var(--faint); +} + +.panel-tag.is-reference { + color: var(--text); + font-weight: 650; +} + +.section-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.7px; + font-weight: 650; + color: var(--faint); + margin: 2px 0; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.btn { + font-family: var(--sans); + font-size: 13px; + font-weight: 600; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: 8px 12px; + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; +} + +.btn:hover { border-color: var(--brand); background: var(--brand-soft); } +.btn:active { background: var(--panel-2); } +.btn:disabled { opacity: 0.55; cursor: default; } +.btn-primary { + border-color: var(--brand-ink); + background: var(--brand-ink); + color: #ffffff; +} +.btn-primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); } + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px; +} + +.feed { + flex: 1; + overflow-y: auto; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + font-size: 13px; + line-height: 1.5; +} + +.empty { color: var(--faint); font-style: italic; } + +.tstamp { + font-family: var(--mono); + font-size: 10.5px; + color: var(--faint); + white-space: nowrap; +} + +.statusline { + display: flex; + flex-wrap: wrap; + gap: 3px 20px; + font-size: 12.5px; + color: var(--muted); +} +.statusline .live { color: var(--brand-ink); font-weight: 600; } diff --git a/resource-manager-mqtt-bridge-ruby/rm/script/.keep b/resource-manager-mqtt-bridge-ruby/rm/script/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb new file mode 100644 index 0000000..d8c855c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb @@ -0,0 +1,32 @@ +FactoryBot.define do + factory :resource do + s2_identifier { SecureRandom.uuid } + configuration do + build(:stekker_mqtt_connect).data.to_dynamic + end + active + + trait :one_phase do + configuration do + build( + :stekker_mqtt_connect, + data: { + name: "My charger - #1", + voltage: 230, + number_of_phases: 1, + minimum_current: 0, + maximum_current: 16, + }, + ).data.to_dynamic + end + end + + trait :active do + disconnected_at { nil } + end + + trait :inactive do + disconnected_at { Time.current } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb new file mode 100644 index 0000000..27f887d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :s2_frbc_instruction, class: "S2::Messages::FRBCInstruction" do + initialize_with { S2::Messages::FRBCInstruction.from_dynamic!(attributes.with_indifferent_access) } + + message_id { generate(:uuid) } + message_type { S2::Messages::FRBCInstructionMessageType::FRBCInstruction } + + abnormal_condition { false } + actuator_id { generate(:uuid) } + execution_time { Time.current.iso8601 } + id { generate(:uuid) } + operation_mode { generate(:uuid) } + operation_mode_factor { 1 } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb new file mode 100644 index 0000000..205bbe4 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :s2_handshake, class: "S2::Messages::Handshake" do + initialize_with { S2::Messages::Handshake.from_dynamic!(attributes.with_indifferent_access) } + message_id { generate(:uuid) } + message_type { S2::Messages::HandshakeMessageType::Handshake } + cem + supported_protocol_versions { S2.supported_protocol_versions } + + trait :cem do + role { S2::Messages::EnergyManagementRole::Cem } + end + + trait :rm do + role { S2::Messages::EnergyManagementRole::Rm } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb new file mode 100644 index 0000000..a5dccf1 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :s2_handshake_response, class: "S2::Messages::HandshakeResponse" do + initialize_with { S2::Messages::HandshakeResponse.from_dynamic!(attributes.with_indifferent_access) } + + message_id { generate(:uuid) } + message_type { S2::Messages::HandshakeResponseMessageType::HandshakeResponse } + selected_protocol_version { "0.0.2-beta" } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb new file mode 100644 index 0000000..81acd96 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb @@ -0,0 +1,33 @@ +FactoryBot.define do + factory :s2_reception_status, class: "S2::Messages::ReceptionStatus" do + initialize_with do + S2::Messages::ReceptionStatus.from_dynamic!( + attributes.stringify_keys, + ) + end + + message_type { S2::Messages::ReceptionStatusMessageType::ReceptionStatus } + subject_message_id { "00000000-0000-0000-0000-000000000000" } + ok + + trait :invalid_content do + status { S2::Messages::ReceptionStatusValues::InvalidContent } + end + + trait :invalid_data do + status { S2::Messages::ReceptionStatusValues::InvalidData } + end + + trait :invalid_message do + status { S2::Messages::ReceptionStatusValues::InvalidMessage } + end + + trait :ok do + status { S2::Messages::ReceptionStatusValues::Ok } + end + + trait :permanent_error do + status { S2::Messages::ReceptionStatusValues::PermanentError } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb new file mode 100644 index 0000000..6f7cd7d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb @@ -0,0 +1,39 @@ +FactoryBot.define do + factory :s2_select_control_type, class: "S2::Messages::SelectControlType" do + initialize_with do + S2::Messages::SelectControlType.from_dynamic!(attributes.with_indifferent_access) + end + + message_id { generate(:uuid) } + message_type { S2::Messages::SelectControlTypeMessageType::SelectControlType } + frbc + + trait :frbc do + control_type { S2::Messages::ControlType::FillRateBasedControl } + end + + trait :ddbc do + control_type { S2::Messages::ControlType::DemandDrivenBasedControl } + end + + trait :no_selection do + control_type { S2::Messages::ControlType::NoSelection } + end + + trait :not_controlable do + control_type { S2::Messages::ControlType::NotControlable } + end + + trait :ombc do + control_type { S2::Messages::ControlType::OperationModeBasedControl } + end + + trait :pebc do + control_type { S2::Messages::ControlType::PowerEnvelopeBasedControl } + end + + trait :ppbc do + control_type { S2::Messages::ControlType::PowerProfileBasedControl } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb new file mode 100644 index 0000000..57026f7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + sequence :uuid do |_n| + SecureRandom.uuid + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb new file mode 100644 index 0000000..1dadd92 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb @@ -0,0 +1,156 @@ +describe ProcessFRBCInstructionJob do + describe "#perform" do + it "publishes an MQTT power_setpoint message when charging" do + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + frbc_instruction = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.second.id, # Charging mode + operation_mode_factor: 0.5, + execution_time: "2024-01-01T12:00:00Z", + duration: 10.minutes.in_milliseconds, + ) + + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now( + resource_id: resource.s2_identifier, + frbc_system_description_json: frbc_system_description.to_json, + frbc_instruction_json: frbc_instruction.to_json, + ) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/power_setpoints", + message: { + data: { + ends_at: "2024-01-01T12:10:00Z", + reference: frbc_instruction.id, + starts_at: "2024-01-01T12:00:00Z", + unit: "W", + value: 5_520, + }, + message_type: "power_setpoint", + resource_id: resource.s2_identifier, + }, + retain: false, + qos: 1, + ) + end + end + + it "publishes an MQTT power_setpoint message when charging with power floor set" do + resource = create( + :resource, + configuration: build(:stekker_mqtt_connect).data.to_dynamic.tap do |config| + config[:minimum_current] = 6 + end, + ) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + frbc_instruction = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.second.id, # Charging mode + operation_mode_factor: 0.5, + execution_time: "2024-01-01T12:00:00Z", + duration: 10.minutes.in_milliseconds, + ) + + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now( + resource_id: resource.s2_identifier, + frbc_system_description_json: frbc_system_description.to_json, + frbc_instruction_json: frbc_instruction.to_json, + ) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/power_setpoints", + message: { + data: { + reference: frbc_instruction.id, + starts_at: "2024-01-01T12:00:00Z", + ends_at: "2024-01-01T12:10:00Z", + unit: "W", + value: 7_590, + }, + message_type: "power_setpoint", + resource_id: resource.s2_identifier, + }, + retain: false, + qos: 1, + ) + end + end + + it "publishes an MQTT power_setpoint message when off" do + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + frbc_instruction = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, # Off mode + operation_mode_factor: 1.0, + ) + + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now( + resource_id: resource.s2_identifier, + frbc_system_description_json: frbc_system_description.to_json, + frbc_instruction_json: frbc_instruction.to_json, + ) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/power_setpoints", + message: { + data: { + ends_at: nil, + reference: frbc_instruction.id, + starts_at: frbc_instruction.execution_time, + unit: "W", + value: 0, + }, + message_type: "power_setpoint", + resource_id: resource.s2_identifier, + }, + retain: false, + qos: 1, + ) + end + end + + it "publishes an MQTT instruction_update message with status ACCEPTED" do + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + frbc_instruction = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.second.id, # Charging mode + operation_mode_factor: 0.5, + ) + + freeze_time do + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now( + resource_id: resource.s2_identifier, + frbc_system_description_json: frbc_system_description.to_json, + frbc_instruction_json: frbc_instruction.to_json, + ) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/instruction_updates", + message: { + data: { + reference: frbc_instruction.id, + updated_at: Time.current.iso8601, + status: Stekker::MQTT::Messages::Status::Accepted, + }, + message_type: "instruction_update", + resource_id: resource.s2_identifier, + }, + retain: false, + qos: 1, + ) + end + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb new file mode 100644 index 0000000..acf3d3d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb @@ -0,0 +1,21 @@ +describe PublishS2ReadyNotificationJob do + describe "#perform" do + it "publishes an MQTT s2_ready_notification message" do + resource = create(:resource) + + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now(resource_id: resource.s2_identifier) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/s2_ready_notifications", + message: { + message_type: "s2_ready_notification", + resource_id: resource.s2_identifier, + }, + retain: false, + qos: 0, + ) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb new file mode 100644 index 0000000..121c70d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb @@ -0,0 +1,26 @@ +describe SendHeartbeatPowerMeasurementJob do + it "publishes a zero-value MQTT power measurement" do + resource = create(:resource, :active) + + freeze_time do + with_fake_mqtt_client do |mqtt_client| + described_class.perform_now(resource_id: resource.s2_identifier) + + expect(mqtt_client).to have_published_mqtt_message( + topic: "stekker/resources/#{resource.s2_identifier}/power_measurements", + message: { + message_type: "power_measurement", + resource_id: resource.s2_identifier, + data: { + sampled_at: Time.current.iso8601, + unit: "W", + value: 0, + }, + }, + retain: false, + qos: 0, + ) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb new file mode 100644 index 0000000..8615d4f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb @@ -0,0 +1,39 @@ +describe SendHeartbeatPowerMeasurementsJob do + it "enqueues heartbeat jobs for active resources needing a heartbeat" do + create(:resource, :active, last_power_measurement_at: nil) + _recent = create(:resource, :active, last_power_measurement_at: 800.seconds.ago) + _inactive = create(:resource, :inactive) + + allow(ActiveJob).to receive(:perform_all_later) + + described_class.perform_now + + expect(ActiveJob).to have_received(:perform_all_later).with( + [an_instance_of(SendHeartbeatPowerMeasurementJob)], + ) + end + + it "does not enqueue heartbeats when all resources have recent measurements" do + create(:resource, :active, last_power_measurement_at: 800.seconds.ago) + create(:resource, :active, last_power_measurement_at: 5.minutes.ago) + + allow(ActiveJob).to receive(:perform_all_later) + + described_class.perform_now + + expect(ActiveJob).to have_received(:perform_all_later).with([]) + end + + it "enqueues a heartbeat for a resource with a stale power measurement" do + create(:resource, :active, last_power_measurement_at: 20.minutes.ago) + _recent = create(:resource, :active, last_power_measurement_at: 800.seconds.ago) + + allow(ActiveJob).to receive(:perform_all_later) + + described_class.perform_now + + expect(ActiveJob).to have_received(:perform_all_later).with( + [an_instance_of(SendHeartbeatPowerMeasurementJob)], + ) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb new file mode 100644 index 0000000..0cb62d3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb @@ -0,0 +1,12 @@ +describe BaseFactory do + describe ".commodity_quantity_for" do + it "always returns 3_PHASE_SYMMETRIC regardless of number_of_phases" do + [1, 2, 3].each do |phases| + resource = build(:resource, configuration: { "number_of_phases" => phases }) + + expect(described_class.commodity_quantity_for(resource)) + .to eq(S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb new file mode 100644 index 0000000..f7326c6 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb @@ -0,0 +1,35 @@ +describe ErrorNotifier do + describe "::notify_error" do + let(:error) do + raise "Test error" + rescue StandardError => e + e + end + + it "logs the error message and backtrace locally" do + allow(Rails.logger).to receive(:error) + + described_class.notify_error(error) + + expect(Rails.logger).to have_received(:error).with("RuntimeError: Test error") + expect(Rails.logger).to have_received(:error).with(error.backtrace.join("\n")) + end + + it "omits the backtrace outside of local environments" do + allow(Rails.logger).to receive(:error) + allow(Rails.env).to receive(:local?).and_return(false) + + described_class.notify_error(error) + + expect(Rails.logger).to have_received(:error).with("RuntimeError: Test error") + expect(Rails.logger).not_to have_received(:error).with(error.backtrace.join("\n")) + end + + it "accepts extra context and tags without raising" do + allow(Rails.logger).to receive(:error) + + expect { described_class.notify_error(error, extra: { http_status: 404 }, tags: { resource_id: "RM-1" }) } + .not_to raise_error + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb new file mode 100644 index 0000000..17e4fd8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb @@ -0,0 +1,53 @@ +describe InstructionStatusTracker do + describe "#already_relayed?" do + it "returns false for an unseen combination" do + tracker = described_class.new + + result = tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + + expect(result).to be false + end + + it "returns true for a previously seen combination" do + tracker = described_class.new + tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + + result = tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + + expect(result).to be true + end + + it "distinguishes different statuses for the same instruction" do + tracker = described_class.new + tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + + result = tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "SUCCEEDED") + + expect(result).to be false + end + + it "distinguishes different resources" do + tracker = described_class.new + tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + + result = tracker.already_relayed?(resource_id: "res-2", instruction_id: "instr-1", status: "STARTED") + + expect(result).to be false + end + end + + describe "#clear_resource" do + it "removes all tracked entries for a resource" do + tracker = described_class.new + tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED") + tracker.already_relayed?(resource_id: "res-2", instruction_id: "instr-2", status: "STARTED") + + tracker.clear_resource("res-1") + + expect(tracker.already_relayed?(resource_id: "res-1", instruction_id: "instr-1", status: "STARTED")) + .to be false + expect(tracker.already_relayed?(resource_id: "res-2", instruction_id: "instr-2", status: "STARTED")) + .to be true + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb new file mode 100644 index 0000000..fa134af --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb @@ -0,0 +1,50 @@ +describe MessageRelayer do + describe "#relay_message" do + it "sends the message to the found connected connection" do + resource_id = "resource-123" + connection = instance_double(S2::Connection) + allow(connection).to receive(:send_message) + allow(connection).to receive(:status).and_return(:connected) + connection_registry = ConnectionRegistry.new + connection_registry.register_connection("resource-123", connection) + relayer = described_class.new(connection_registry) + message = double("S2 message", message_type: "PowerMeasurement", to_json: "{}") + + relayer.relay_message(resource_id:, message:) + + expect(connection).to have_received(:send_message).with(message) + end + + it "logs a warning if no connection is registered" do + resource_id = "nonexistent-resource" + connection_registry = ConnectionRegistry.new + relayer = described_class.new(connection_registry) + message = double("S2 message", message_type: "PowerMeasurement", to_json: "{}") + allow(Rails.logger).to receive(:warn) + + relayer.relay_message(resource_id:, message:) + + expect(Rails.logger) + .to have_received(:warn) + .with("[MessageRelayer] No connected connection found for resource_id #{resource_id}") + end + + it "logs a warning if registered connection is not connected" do + resource_id = "resource-123" + connection = instance_double(S2::Connection) + allow(connection).to receive(:send_message) + allow(connection).to receive(:status).and_return(:disconnected) + + connection_registry = ConnectionRegistry.new + relayer = described_class.new(connection_registry) + message = double("S2 message", message_type: "PowerMeasurement", to_json: "{}") + allow(Rails.logger).to receive(:warn) + + relayer.relay_message(resource_id:, message:) + + expect(Rails.logger) + .to have_received(:warn) + .with("[MessageRelayer] No connected connection found for resource_id #{resource_id}") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb new file mode 100644 index 0000000..48110bf --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb @@ -0,0 +1,490 @@ +describe MQTTMessageHandler do + describe "connect" do + it "registers a new Resource" do + valid_message = build(:stekker_mqtt_connect) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + expect { handler.handle_message(valid_message.to_json) } + .to change(Resource, :count).by(1) + + resource = Resource.last + expect(resource.s2_identifier).to eq(valid_message.resource_id) + expect(resource.configuration).to eq(valid_message.data.to_dynamic) + end + + it "updates an existing Resource" do + existing_resource = create(:resource, configuration: { old: "configuration" }, disconnected_at: Time.current) + valid_message = build(:stekker_mqtt_connect, resource_id: existing_resource.s2_identifier) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + expect { handler.handle_message(valid_message.to_json) } + .not_to change(Resource, :count) + + existing_resource.reload + expect(existing_resource.configuration).to eq(valid_message.data.to_dynamic) + expect(existing_resource.disconnected_at).to be_nil + end + + it "reconnects the S2 connection" do + valid_message = build(:stekker_mqtt_connect) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + supervisor = instance_double(Supervisor) + allow(Supervisor).to receive(:instance).and_return(supervisor) + allow(supervisor).to receive(:find_connection).and_return(nil) + allow(supervisor).to receive(:reconnect) + + handler.handle_message(valid_message.to_json) + + expect(supervisor).to have_received(:reconnect).with(valid_message.resource_id) + end + + it "leaves a live session alone when the same connect repeats" do + valid_message = build(:stekker_mqtt_connect) + existing_resource = create( + :resource, + s2_identifier: valid_message.resource_id, + configuration: valid_message.data.to_dynamic, + disconnected_at: nil, + ) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + supervisor = instance_double(Supervisor) + connection = instance_double(S2::Connection, status: :connected) + allow(Supervisor).to receive(:instance).and_return(supervisor) + allow(supervisor).to receive(:find_connection).with(existing_resource.s2_identifier).and_return(connection) + allow(supervisor).to receive(:reconnect) + + handler.handle_message(valid_message.to_json) + + expect(supervisor).not_to have_received(:reconnect) + end + + it "reconnects a live session when the configuration changes" do + valid_message = build(:stekker_mqtt_connect) + existing_resource = create( + :resource, + s2_identifier: valid_message.resource_id, + configuration: { "name" => "Old charger", "voltage" => 230 }, + disconnected_at: nil, + ) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + supervisor = instance_double(Supervisor) + connection = instance_double(S2::Connection, status: :connected) + allow(Supervisor).to receive(:instance).and_return(supervisor) + allow(supervisor).to receive(:find_connection).with(existing_resource.s2_identifier).and_return(connection) + allow(supervisor).to receive(:reconnect) + + handler.handle_message(valid_message.to_json) + + expect(supervisor).to have_received(:reconnect).with(valid_message.resource_id) + end + end + + describe "disconnect" do + it "marks the Resource as disconnected" do + existing_resource = create(:resource, disconnected_at: nil) + valid_message = build(:stekker_mqtt_disconnect, resource_id: existing_resource.s2_identifier) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + freeze_time do + expect { handler.handle_message(valid_message.to_json) } + .to change { existing_resource.reload.disconnected_at }.from(nil).to(Time.current) + end + end + + it "disconnects the S2 connection" do + existing_resource = create(:resource) + valid_message = build(:stekker_mqtt_disconnect, resource_id: existing_resource.s2_identifier) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + supervisor = instance_double(Supervisor) + allow(Supervisor).to receive(:instance).and_return(supervisor) + allow(supervisor).to receive(:disconnect) + + handler.handle_message(valid_message.to_json) + + expect(supervisor).to have_received(:disconnect).with(existing_resource.s2_identifier) + end + + it "does nothing if the Resource does not exist" do + valid_message = build(:stekker_mqtt_disconnect) + message_relayer = instance_double(MessageRelayer) + handler = described_class.new(message_relayer:) + + expect { handler.handle_message(valid_message.to_json) } + .not_to change(Resource, :count) + end + + it "clears tracked instruction statuses for the disconnected resource" do + existing_resource = create(:resource, disconnected_at: nil) + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + instruction_status_tracker = InstructionStatusTracker.new + handler = described_class.new(message_relayer:, instruction_status_tracker:) + + instruction_update = build(:stekker_mqtt_instruction_update, resource_id: existing_resource.s2_identifier) + handler.handle_message(instruction_update.to_json) + + supervisor = instance_double(Supervisor) + allow(Supervisor).to receive(:instance).and_return(supervisor) + allow(supervisor).to receive(:disconnect) + + disconnect_message = build(:stekker_mqtt_disconnect, resource_id: existing_resource.s2_identifier) + handler.handle_message(disconnect_message.to_json) + + handler.handle_message(instruction_update.to_json) + + expect(message_relayer).to have_received(:relay_message).twice + end + end + + describe "power_measurement" do + it "relays a 3-phase power measurement with ElectricPower3_PhaseSymmetric" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_measurement) + create(:resource, s2_identifier: valid_message.resource_id) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(resource_id).to eq(valid_message.resource_id) + expect(message).to be_a(S2::Messages::PowerMeasurement) + expect(message.attributes).to include( + message_type: S2::Messages::PowerMeasurementMessageType::PowerMeasurement, + measurement_timestamp: valid_message.data.sampled_at, + values: [ + a_hash_including( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + value: 1500, + ), + ], + ) + end + end + + it "still emits ElectricPower3_PhaseSymmetric for a 1-phase resource (hardcoded in BaseFactory)" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_measurement) + create(:resource, :one_phase, s2_identifier: valid_message.resource_id) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(message.attributes).to include( + values: [ + a_hash_including( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + value: 1500, + ), + ], + ) + end + end + + it "records the power measurement timestamp on the resource" do + resource = create(:resource) + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_measurement, resource_id: resource.s2_identifier) + + freeze_time do + handler.handle_message(valid_message.to_json) + + expect(resource.reload.last_power_measurement_at).to eq(Time.current) + end + end + + it "instruments an error for invalid unit" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + allow(ActiveSupport::Notifications).to receive(:instrument) + handler = described_class.new(message_relayer:) + handler.raise_handling_errors = false + + invalid_message = <<~JSON + { + "message_type": "power_measurement", + "resource_id": "#{generate(:uuid)}", + "data": { + "sampled_at": "#{Time.current.iso8601}", + "value": 1500, + "unit": "kW" + } + } + JSON + + handler.handle_message(invalid_message) + + expect(message_relayer).not_to have_received(:relay_message) + expect(ActiveSupport::Notifications).to have_received(:instrument) do |event_name, payload| + expect(event_name).to eq("message_errored.message_handler.mqtt.stekker") + expect(payload[:payload]).to eq(invalid_message) + expect(payload[:exception]).to be_a(Dry::Struct::Error) + expect(payload[:exception].message).to match(/invalid type for.+unit/i) + end + end + end + + describe "power_forecast" do + it "does not relay power forecast messages when provides_forecast is disabled" do + freeze_time + + original = Rails.configuration.s2.provides_forecast + Rails.configuration.s2.provides_forecast = false + + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_forecast) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).not_to have_received(:relay_message) + ensure + Rails.configuration.s2.provides_forecast = original + end + + it "relays a 3-phase power forecast with ElectricPower3_PhaseSymmetric" do + freeze_time + + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_forecast) + create(:resource, s2_identifier: valid_message.resource_id) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(resource_id).to eq(valid_message.resource_id) + expect(message).to be_a(S2::Messages::PowerForecast) + + expect(message).to have_attributes( + message_type: S2::Messages::PowerForecastMessageType::PowerForecast, + start_time: valid_message.data.start_at, + ) + + expect(message.elements[0]).to have_attributes( + duration: 10.minutes.in_milliseconds, + power_values: [ + an_object_having_attributes( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + value_expected: 11_040, + ), + ], + ) + + expect(message.elements[1]).to have_attributes( + duration: 20.minutes.in_milliseconds, + power_values: [ + an_object_having_attributes( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + value_expected: 6_720, + ), + ], + ) + + expect(message.elements[2]).to have_attributes( + duration: (23.hours + 25.minutes).in_milliseconds, + power_values: [ + an_object_having_attributes( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + value_expected: 0, + ), + ], + ) + end + end + + it "still emits ElectricPower3_PhaseSymmetric for a 1-phase resource (hardcoded in BaseFactory)" do + freeze_time + + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_power_forecast) + create(:resource, :one_phase, s2_identifier: valid_message.resource_id) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(message.elements[0]).to have_attributes( + power_values: [ + an_object_having_attributes( + commodity_quantity: S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + ), + ], + ) + end + end + + it "instruments an error for invalid unit" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + allow(ActiveSupport::Notifications).to receive(:instrument) + handler = described_class.new(message_relayer:) + handler.raise_handling_errors = false + + invalid_message = <<~JSON + { + "message_type": "power_forecast", + "resource_id": "#{generate(:uuid)}", + "data": { + "start_at": "#{Time.current.iso8601}", + "unit": "kW", + "values": [ + { "timestamp": "#{5.minutes.from_now.iso8601}", "value": 11040 }, + { "timestamp": "#{15.minutes.from_now.iso8601}", "value": 6720 }, + { "timestamp": "#{35.minutes.from_now.iso8601}", "value": 0 } + ] + } + } + JSON + + handler.handle_message(invalid_message) + + expect(message_relayer).not_to have_received(:relay_message) + expect(ActiveSupport::Notifications).to have_received(:instrument) do |event_name, payload| + expect(event_name).to eq("message_errored.message_handler.mqtt.stekker") + expect(payload[:payload]).to eq(invalid_message) + expect(payload[:exception]).to be_a(Dry::Struct::Error) + expect(payload[:exception].message).to match(/invalid type for.+unit/i) + end + end + end + + describe "charge_status" do + it "relays charge status messages as StorageStatus messages" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_charge_status) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(resource_id).to eq(valid_message.resource_id) + expect(message).to be_a(S2::Messages::FRBCStorageStatus) + expect(message.attributes).to include( + message_type: S2::Messages::FRBCStorageStatusMessageType::FRBCStorageStatus, + measurement_timestamp: valid_message.data.sampled_at, + present_fill_level: valid_message.data.value, + ) + end + end + end + + describe "charge_target" do + it "relays charge target messages as FRBC.FillLevelTargetProfile messages" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_charge_target) + starts_at = Time.zone.parse(valid_message.data.starts_at) + ends_at = Time.zone.parse(valid_message.data.ends_at) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(resource_id).to eq(valid_message.resource_id) + expect(message).to be_a(S2::Messages::FRBCFillLevelTargetProfile) + expect(message.attributes).to include( + message_type: S2::Messages::FRBCFillLevelTargetProfileMessageType::FRBCFillLevelTargetProfile, + start_time: valid_message.data.starts_at, + ) + + expect(message.elements).to include( + an_object_having_attributes( + duration: (ends_at - starts_at - 15.minutes).in_milliseconds, + fill_level_range: an_object_having_attributes( + start_of_range: valid_message.data.direct_percentage, + end_of_range: valid_message.data.smart_percentage, + ), + ), + ) + + expect(message.elements).to include( + an_object_having_attributes( + duration: 15.minutes.in_milliseconds, + fill_level_range: an_object_having_attributes( + start_of_range: valid_message.data.smart_percentage, + end_of_range: valid_message.data.smart_percentage, + ), + ), + ) + end + end + end + + describe "instruction_update" do + it "relays instruction update messages as InstructionStatusUpdate messages" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + handler = described_class.new(message_relayer:) + + valid_message = build(:stekker_mqtt_instruction_update) + + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message) do |resource_id:, message:| + expect(resource_id).to eq(valid_message.resource_id) + expect(message).to be_a(S2::Messages::InstructionStatusUpdate) + expect(message.attributes).to include( + message_type: S2::Messages::InstructionStatusUpdateMessageType::InstructionStatusUpdate, + instruction_id: valid_message.data.reference, + timestamp: valid_message.data.updated_at, + status_type: valid_message.data.status, + ) + end + end + + it "skips duplicate instruction updates for the same resource, reference and status" do + message_relayer = instance_double(MessageRelayer) + allow(message_relayer).to receive(:relay_message) + instruction_status_tracker = InstructionStatusTracker.new + handler = described_class.new(message_relayer:, instruction_status_tracker:) + + valid_message = build(:stekker_mqtt_instruction_update) + + handler.handle_message(valid_message.to_json) + handler.handle_message(valid_message.to_json) + + expect(message_relayer).to have_received(:relay_message).once + end + end + + describe "database connection isolation" do + it "handles each message inside the Rails executor so it doesn't reuse a stale connection" do + valid_message = build(:stekker_mqtt_connect) + handler = described_class.new(message_relayer: instance_double(MessageRelayer)) + allow(Supervisor).to receive(:instance).and_return(instance_double(Supervisor, reconnect: nil, find_connection: nil)) + allow(Rails.application.executor).to receive(:wrap).and_call_original + + handler.handle_message(valid_message.to_json) + + expect(Rails.application.executor).to have_received(:wrap) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb new file mode 100644 index 0000000..4d1bc7d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb @@ -0,0 +1,58 @@ +describe MQTTMessageLogger do + describe "#log_inbound" do + it "logs inbound MQTT message with topic, resource_id, and message" do + logger = instance_double(EcsLogging::Logger, info: nil) + + described_class.new(logger).log_inbound( + topic: "stekker/resources/ABC123/measurements", + message: '{"state": "charging"}', + ) + + expect(logger).to have_received(:info).with( + "Inbound MQTT message", + mqtt: { + topic: "stekker/resources/ABC123/measurements", + resource_id: "ABC123", + message: '{"state": "charging"}', + }, + ) + end + + it "omits resource_id when topic does not match expected pattern" do + logger = instance_double(EcsLogging::Logger, info: nil) + + described_class.new(logger).log_inbound( + topic: "other/topic/format", + message: '{"data": "value"}', + ) + + expect(logger).to have_received(:info).with( + "Inbound MQTT message", + mqtt: { + topic: "other/topic/format", + message: '{"data": "value"}', + }, + ) + end + end + + describe "#log_outbound" do + it "logs outbound MQTT message with topic, resource_id, and message" do + logger = instance_double(EcsLogging::Logger, info: nil) + + described_class.new(logger).log_outbound( + topic: "stekker/resources/ABC123/connects", + message: '{"message_type": "connect"}', + ) + + expect(logger).to have_received(:info).with( + "Outbound MQTT message", + mqtt: { + topic: "stekker/resources/ABC123/connects", + resource_id: "ABC123", + message: '{"message_type": "connect"}', + }, + ) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb new file mode 100644 index 0000000..7418db7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb @@ -0,0 +1,65 @@ +describe S2::Connection, :async do + describe "#connect" do + it "uses exponential backoff for reconnection attempts up to 1 hour" do + ws = FakeWebSocket.new + connection_attempts = 0 + sleep_durations = [] + + allow(Async::WebSocket::Client).to receive(:connect) do |_endpoint, &block| + connection_attempts += 1 + raise Errno::ECONNREFUSED if connection_attempts < 15 + + block.call(ws) + end + + resource_id = SecureRandom.uuid + ws_url = "ws://example.com/#{resource_id}" + + connection = described_class.new(resource_id:, ws_url:) + + allow(connection).to receive(:sleep) { |duration| sleep_durations << duration } + + task = Async do + connection.connect + end + + Async::Task.current.sleep 0.1 + + connection.disconnect + task.stop + + expect(sleep_durations).to eq([5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600, 3600, 3600, 3600]) + end + + it "reuses the same endpoint across reconnection attempts" do + ws = FakeWebSocket.new + connection_attempts = [] + + allow(Async::WebSocket::Client).to receive(:connect) do |endpoint, &block| + connection_attempts << endpoint.object_id + raise Errno::ECONNREFUSED if connection_attempts.size < 3 + + block.call(ws) + end + + resource_id = SecureRandom.uuid + ws_url = "ws://example.com/#{resource_id}" + + connection = described_class.new(resource_id:, ws_url:) + + allow(connection).to receive(:sleep) + + task = Async do + connection.connect + end + + Async::Task.current.sleep 0.1 + + connection.disconnect + task.stop + + expect(connection_attempts.size).to be >= 3 + expect(connection_attempts.uniq.size).to eq(1) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb new file mode 100644 index 0000000..1bbaddd --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb @@ -0,0 +1,72 @@ +describe S2::MessageFactory do + describe "::create_message" do + it "creates a message object from valid JSON data" do + json_data = <<~JSON + { + "message_type": "Handshake", + "message_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "role": "RM", + "supported_protocol_versions": ["1.0", "1.1"] + } + JSON + + message = described_class.create_message(json_data) + + expect(message).to be_a(S2::Messages::Handshake) + + expect(message).to have_attributes( + message_type: "Handshake", + message_id: "ce7b430c-25be-41ec-b97c-ac0258aff498", + role: "RM", + supported_protocol_versions: %w[1.0 1.1], + ) + end + + it "raises InvalidMessageFormatError for invalid JSON data" do + invalid_json_data = "{ invalid json }" + + expect { described_class.create_message(invalid_json_data) } + .to raise_error(S2::MessageFactory::InvalidMessageFormat, "Invalid JSON") + end + + it "raises MissingMessageTypeError when message_type is missing" do + json_data = <<~JSON + { + "message_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "role": "RM", + "supported_protocol_versions": ["1.0", "1.1"] + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error(S2::MessageFactory::MissingMessageType, "Message type not provided") + end + + it "raises UnsupportedMessageTypeError for unsupported message_type" do + json_data = <<~JSON + { + "message_type": "UnknownType", + "message_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "role": "RM", + "supported_protocol_versions": ["1.0", "1.1"] + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error(S2::MessageFactory::UnsupportedMessageType, /Message type not supported: UnknownType/) + end + + it "raises InvalidMessagePayloadError for missing required fields" do + json_data = <<~JSON + { + "message_type": "Handshake", + "message_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "supported_protocol_versions": ["1.0", "1.1"] + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error(S2::MessageFactory::InvalidMessagePayload, /Field missing in payload: role/) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb new file mode 100644 index 0000000..f8ebfbc --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb @@ -0,0 +1,118 @@ +describe S2::MessageHandler::Dispatching do + describe "::on" do + it "registers a handler for the specified message class" do + handler_block = proc {} + + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + + on S2::Messages::Handshake, &handler_block + end + + expect(message_handler_class.handlers[S2::Messages::Handshake.name]).to eq(handler_block) + end + + it "raises an error when defining a second handler for the same message" do + handler_block = proc {} + + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + + on S2::Messages::Handshake, &handler_block + end + + expect { message_handler_class.on(S2::Messages::Handshake, &handler_block) } + .to raise_error(/Handler already registered for S2::Messages::Handshake/) + end + end + + describe "#dispatch_message" do + it "invokes the registered handler for the message" do + handler_invoked = false + handler_block = proc { |message| handler_invoked = message.instance_of?(S2::Messages::Handshake) } + + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + + on S2::Messages::Handshake, &handler_block + end + + handler = message_handler_class.new + message = build(:s2_handshake) + + handler.dispatch_message(message) + expect(handler_invoked).to be true + end + + it "raises an error if no handler is registered for the message" do + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + end + + handler = message_handler_class.new + message = build(:s2_handshake) + + expect { handler.dispatch_message(message) } + .to raise_error( + S2::MessageHandler::Dispatching::HandlerNotFound, + /No handler registered for S2::Messages::Handshake/, + ) + end + + it "raises an error if no handler is registered for a message without message_id" do + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + end + + handler = message_handler_class.new + message = build(:s2_reception_status) + + expect { handler.dispatch_message(message) } + .to raise_error( + S2::MessageHandler::Dispatching::HandlerNotFound, + /No handler registered for S2::Messages::ReceptionStatus/, + ) + end + + it "allows passing a custom type" do + handler_invoked = false + handler_block = proc { |message| handler_invoked = message.instance_of?(S2::Messages::Handshake) } + + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + + on "my_custom_message_type", &handler_block + end + + handler = message_handler_class.new + message = build(:s2_handshake) + + handler.dispatch_message(message, type: "my_custom_message_type") + expect(handler_invoked).to be true + end + + it "raises an ArgumentError when the specified type is nil" do + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + end + + handler = message_handler_class.new + message = build(:s2_handshake) + + expect { handler.dispatch_message(message, type: nil) } + .to raise_error(ArgumentError, "Missing message type") + end + + it "raises an ArgumentError when the specified type is blank" do + message_handler_class = Class.new do + include S2::MessageHandler::Dispatching + end + + handler = message_handler_class.new + message = build(:s2_handshake) + + expect { handler.dispatch_message(message, type: "") } + .to raise_error(ArgumentError, "Missing message type") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb new file mode 100644 index 0000000..ae0e685 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb @@ -0,0 +1,234 @@ +describe S2::MessageHandler::ErrorHandling do + it "replies to InvalidMessageFormat with invalid_data status" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise S2::MessageFactory::InvalidMessageFormat.new("test_id", "Invalid format") + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + handler.rescue_with_handler(S2::MessageFactory::InvalidMessageFormat.new("Invalid format", "test_id")) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: "test_id", + status: S2::Messages::ReceptionStatusValues::InvalidData, + diagnostic_label: "Invalid format", + ) + end + end + + it "replies to MissingMessageType with invalid_message status" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise S2::MessageFactory::MissingMessageType.new("test_id", "Missing type") + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + handler.rescue_with_handler(S2::MessageFactory::MissingMessageType.new("Missing type", "test_id")) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: "test_id", + status: S2::Messages::ReceptionStatusValues::InvalidMessage, + diagnostic_label: "Missing type", + ) + end + end + + it "replies to InvalidMessagePayload with invalid_message status" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise S2::MessageFactory::InvalidMessagePayload.new("test_id", "Invalid payload") + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + handler.rescue_with_handler(S2::MessageFactory::InvalidMessagePayload.new("Invalid payload", "test_id")) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: "test_id", + status: S2::Messages::ReceptionStatusValues::InvalidMessage, + diagnostic_label: "Invalid payload", + ) + end + end + + it "replies to UnsupportedMessageType with invalid_message status" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise S2::MessageFactory::UnsupportedMessageType.new("test_id", "Unsupported type") + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + handler.rescue_with_handler(S2::MessageFactory::UnsupportedMessageType.new("Unsupported type", "test_id")) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: "test_id", + status: S2::Messages::ReceptionStatusValues::InvalidMessage, + diagnostic_label: "Unsupported type", + ) + end + end + + it "replies to HandlerNotFound with invalid_message status" do + instance_double(S2::Messages::Handshake, message_type: "UnknownType", message_id: "test_id") + + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise S2::MessageHandler::Dispatching::HandlerNotFound.new(message, "test_id") + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + + allow(message_sender).to receive(:send_message) + + handler.rescue_with_handler( + S2::MessageHandler::Dispatching::HandlerNotFound.new("Handler not found!"), + ) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: S2::MessageHandler::Replying::NULL_MESSAGE_ID, + status: S2::Messages::ReceptionStatusValues::InvalidMessage, + diagnostic_label: "Unsupported message type", + ) + end + end + + it "replies to StandardError with temporary_error status" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise StandardError, "General error" + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + begin + raise "General error" + rescue StandardError => e + exception = e + end + + handler.rescue_with_handler(exception) + + expect(message_sender).to have_received(:send_message) do |message| + expect(message).to be_a(S2::Messages::ReceptionStatus) + expect(message).to have_attributes( + subject_message_id: S2::MessageHandler::Replying::NULL_MESSAGE_ID, + status: S2::Messages::ReceptionStatusValues::TemporaryError, + diagnostic_label: nil, + ) + end + end + + it "emits notification when rescuing StandardError" do + test_class = Class.new do + include S2::MessageHandler::ErrorHandling + + reply_with :@message_sender + + def initialize(message_sender:) + @message_sender = message_sender + end + + def test_method + raise StandardError, "General error" + end + end + + message_sender = instance_double(S2::MessageSender) + handler = test_class.new(message_sender: message_sender) + allow(message_sender).to receive(:send_message) + + begin + raise "General error" + rescue StandardError => e + exception = e + end + + callback = ->(*args) do + event = ActiveSupport::Notifications::Event.new(*args) + expect(event.name).to eq("message_errored.session.s2") + expect(event.payload[:exception]).to eq(exception) + end + + ActiveSupport::Notifications.subscribed(callback, "message_errored.session.s2") do + handler.rescue_with_handler(exception) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb new file mode 100644 index 0000000..45710d8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb @@ -0,0 +1,159 @@ +describe S2::MessageHandler::Replying do + describe "::reply_with" do + it "sets the replier to the specified method or instance variable" do + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + end + + expect(message_handler_class.replier).to eq(:@custom_replier) + end + end + + describe "#reply" do + it "sends the reply message using the configured replier" do + replier = instance_double(S2::MessageSender) + allow(replier).to receive(:send_message) + + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + + def initialize(custom_replier:) + @custom_replier = custom_replier + end + end + + handler = message_handler_class.new(custom_replier: replier) + handshake = build(:s2_handshake) + expected_reply_message = build( + :s2_reception_status, + status: S2::Messages::ReceptionStatusValues::InvalidMessage, + subject_message_id: handshake.message_id, + diagnostic_label: "Your message is completely bogus", + ) + + handler.reply( + to: handshake, + status: :invalid_message, + diagnostic_label: "Your message is completely bogus", + ) + + expect(replier).to have_received(:send_message).with(expected_reply_message) + end + + it "raises an error if an unknown status is provided" do + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + + def initialize(custom_replier:) + @custom_replier = custom_replier + end + end + + handler = message_handler_class.new(custom_replier: instance_double(S2::MessageSender)) + handshake = build(:s2_handshake) + + expect { handler.reply(to: handshake, status: :unknown_status) } + .to raise_error(ArgumentError, /Unknown status: unknown_status/) + end + + it "raises an error if no replier is configured" do + message_handler_class = Class.new do + include S2::MessageHandler::Replying + end + + handler = message_handler_class.new + handshake = build(:s2_handshake) + + expect { handler.reply(to: handshake, status: :ok) } + .to raise_error(RuntimeError, /No replier configured/) + end + + it "uses a null message ID if the 'to' message is nil" do + replier = instance_double(S2::MessageSender) + allow(replier).to receive(:send_message) + + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + + def initialize(custom_replier:) + @custom_replier = custom_replier + end + end + + handler = message_handler_class.new(custom_replier: replier) + expected_reply_message = build( + :s2_reception_status, + :ok, + subject_message_id: S2::MessageHandler::Replying::NULL_MESSAGE_ID, + ) + + handler.reply(to: nil, status: :ok) + + expect(replier).to have_received(:send_message).with(expected_reply_message) + end + + it "uses a null message ID if the 'to' message has no message_id" do + replier = instance_double(S2::MessageSender) + allow(replier).to receive(:send_message) + + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + + def initialize(custom_replier:) + @custom_replier = custom_replier + end + end + + handler = message_handler_class.new(custom_replier: replier) + message_without_id = build(:s2_handshake, message_id: "") + expected_reply_message = build( + :s2_reception_status, + :ok, + subject_message_id: S2::MessageHandler::Replying::NULL_MESSAGE_ID, + ) + + handler.reply(to: message_without_id, status: :ok) + + expect(replier).to have_received(:send_message).with(expected_reply_message) + end + end + + describe "#reply_ok" do + it "sends an OK reply message using the configured replier" do + replier = instance_double(S2::MessageSender) + allow(replier).to receive(:send_message) + + message_handler_class = Class.new do + include S2::MessageHandler::Replying + + reply_with :@custom_replier + + def initialize(custom_replier:) + @custom_replier = custom_replier + end + end + + handler = message_handler_class.new(custom_replier: replier) + handshake = build(:s2_handshake) + expected_reply_message = build( + :s2_reception_status, + :ok, + subject_message_id: handshake.message_id, + ) + + handler.reply_ok(to: handshake) + + expect(replier).to have_received(:send_message).with(expected_reply_message) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb new file mode 100644 index 0000000..ffe8b68 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb @@ -0,0 +1,84 @@ +describe S2::MessageHandler do + describe "#handle_message" do + it "invokes a registered handler for the message" do + message_sender = instance_double(S2::MessageSender) + + message_handler_class = Class.new(S2::MessageHandler) do + attr_reader :received_message + + on S2::Messages::Handshake do |message| + @received_message = message + end + end + + handler = message_handler_class.new(message_sender:) + message = build(:s2_handshake) + + handler.handle_message(message.to_json) + expect(handler.received_message).to eq(message) + end + + it "replies to errors with a fitting error reception status" do + message_sender = instance_double(S2::MessageSender) + allow(message_sender).to receive(:send_message) + + message_handler_class = Class.new(described_class) + + handler = message_handler_class.new(message_sender:) + + expect { handler.handle_message("") }.not_to raise_error + + expect(message_sender).to have_received(:send_message) do |reception_status| + expect(reception_status).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::InvalidData, + diagnostic_label: "Invalid JSON", + subject_message_id: S2::MessageHandler::Replying::NULL_MESSAGE_ID, + ) + end + end + + it "calls before, after, and around callbacks" do + message_sender = instance_double(S2::MessageSender) + allow(message_sender).to receive(:send_message) + + callback_order = [] + + message_handler_class = Class.new(S2::MessageHandler) do + before_handle do |payload| + callback_order << [:before, payload] + end + + after_handle do |payload| + callback_order << [:after, payload] + end + + around_handle do |payload, inner| + callback_order << [:around_start, payload] + inner.call + callback_order << [:around_end, payload] + end + + on S2::Messages::Handshake do |message| + callback_order << [:handler, message] + end + end + + handler = message_handler_class.new(message_sender:) + message = build(:s2_handshake) + + payload = message.to_json + handler.handle_message(payload) + + expect(callback_order).to eq( + [ + [:before, payload], + [:around_start, payload], + [:handler, message], + [:around_end, payload], + [:after, payload], + ], + ) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb new file mode 100644 index 0000000..e263812 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb @@ -0,0 +1,107 @@ +describe S2::MessageSender do + describe "#send_message" do + it "sends payload when no success block is given" do + message = build(:s2_handshake) + sent_messages = [] + sender = described_class.new(resource_id: generate(:uuid)) { |json| sent_messages << json } + + sender.send_message(message) + + expect(sent_messages).to eq([message.to_json]) + end + + it "registers a success block and does not log a timeout when OK arrives in time" do + allow(S2.logger).to receive(:warn) + message = build(:s2_handshake) + reception_status = build(:s2_reception_status, subject_message_id: message.message_id) + sent_messages = [] + sender = described_class.new(resource_id: generate(:uuid)) { |json| sent_messages << json } + + called = nil + Async do |task| + sender.send_message(message, timeout: 0.01) { |status| called = status } + sender.receive_reception_status(reception_status) + task.sleep 0.02 + end + + expect(called).to eq(reception_status) + expect(S2.logger).not_to have_received(:warn) + end + + it "logs a warning on timeout when no status arrives" do + allow(S2.logger).to receive(:warn) + message = build(:s2_handshake) + sender = described_class.new(resource_id: generate(:uuid)) do |_json| + # omit sending + end + + Async do |task| + sender.send_message(message, timeout: 0.01) { |_| raise "should not be called" } + task.sleep 0.02 + end + + expect(S2.logger).to have_received(:warn) { |&blk| + expect(blk.call).to match(/Timeout waiting for ReceptionStatus for #{message.message_id}/) + } + end + end + + describe "#receive_reception_status" do + it "only logs for non-OK and does not call the success block" do + allow(S2.logger).to receive(:warn) + message = build(:s2_handshake) + not_ok = build(:s2_reception_status, :permanent_error, subject_message_id: message.message_id) + sender = described_class.new(resource_id: generate(:uuid)) do |_json| + # omit sending + end + + called = nil + Async do |task| + sender.send_message(message, timeout: 0.01) { |status| called = status } + sender.receive_reception_status(not_ok) + task.sleep 0.02 + end + + expect(called).to be_nil + expect(S2.logger).to have_received(:warn) { |&blk| + expect(blk.call).to include("Non-OK ReceptionStatus for #{message.message_id}: PERMANENT_ERROR") + } + end + + it "handles OK when no success block was registered" do + allow(S2.logger).to receive(:debug) + message = build(:s2_handshake) + reception_status = build(:s2_reception_status, subject_message_id: message.message_id) + sent_messages = [] + sender = described_class.new(resource_id: generate(:uuid)) { |json| sent_messages << json } + + Async do + sender.send_message(message) + sender.receive_reception_status(reception_status) + end + + expect(sent_messages).to eq([message.to_json]) + expect(S2.logger).to have_received(:debug) + end + end + + describe "#cleanup" do + it "stops all timeout tasks" do + message = build(:s2_handshake) + sender = described_class.new(resource_id: generate(:uuid)) do |_json| + # omit sending + end + + Async do |task| + sender.send_message(message, timeout: 1) do |_| + # empty callback + end + task.sleep 0.01 + expect(sender.timeout_tasks.size).to eq(1) + sender.cleanup + task.sleep 0.01 + expect(sender.timeout_tasks.size).to eq(0) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb new file mode 100644 index 0000000..5613272 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb @@ -0,0 +1,126 @@ +describe S2::Session do + describe "#start", :async do + it "starts and stops the session" do + ws = FakeWebSocket.new + resource_id = SecureRandom.uuid + session = described_class.new(resource_id:, ws:) + + expect(session.status).to eq(:initialized) + + task = Async { session.start } + Async::Task.current.sleep 0.01 + + expect(session.status).to eq(:running) + + session.stop + Async::Task.current.sleep 0.01 + + expect(session.status).to eq(:stopped) + + task.stop + end + + it "handles incoming messages" do + ws = FakeWebSocket.new + message_handler = instance_double(S2::MessageHandler) + allow(message_handler).to receive(:handle_message) + allow(S2.message_handler_class).to receive(:new).and_return(message_handler) + + resource_id = SecureRandom.uuid + session = described_class.new(resource_id:, ws:) + task = Async { session.start } + Async::Task.current.sleep 0.01 + + ws.simulate_incoming('{"type":"test_message"}') + Async::Task.current.sleep 0.01 + + expect(message_handler).to have_received(:handle_message).with('{"type":"test_message"}') + + session.stop + task.stop + end + + it "sends outgoing messages" do + ws = FakeWebSocket.new + queue = Async::Queue.new + resource_id = SecureRandom.uuid + session = described_class.new(resource_id:, ws:, queue:) + + task = Async { session.start } + + queue.enqueue({ type: "outgoing_message" }) + + Async::Task.current.sleep 0.01 + + expect(ws.sent_messages).to include('{"type":"outgoing_message"}') + + session.stop + task.stop + end + end + + describe "#stop", :async do + it "is idempotent" do + ws = FakeWebSocket.new + resource_id = SecureRandom.uuid + session = described_class.new(resource_id:, ws:) + + expect(session.status).to eq(:initialized) + + task = Async { session.start } + Async::Task.current.sleep 0.01 + + expect(session.status).to eq(:running) + + session.stop + Async::Task.current.sleep 0.01 + + expect(session.status).to eq(:stopped) + + # Call stop again to ensure idempotency + session.stop + Async::Task.current.sleep 0.01 + + expect(session.status).to eq(:stopped) + + task.stop + end + + it "cleans up the message sender" do + ws = FakeWebSocket.new + resource_id = SecureRandom.uuid + message_sender = instance_double(S2::MessageSender) + allow(S2::MessageSender).to receive(:new).and_return(message_sender) + session = described_class.new(resource_id:, ws:) + allow(message_sender).to receive(:cleanup) + + task = Async { session.start } + Async::Task.current.sleep 0.01 + + session.stop + Async::Task.current.sleep 0.01 + + expect(message_sender).to have_received(:cleanup) + + task.stop + end + + it "closes the WebSocket connection" do + ws = FakeWebSocket.new + resource_id = SecureRandom.uuid + session = described_class.new(resource_id:, ws:) + + task = Async { session.start } + Async::Task.current.sleep 0.01 + + expect(ws).not_to be_closed + + session.stop + Async::Task.current.sleep 0.01 + + expect(ws).to be_closed + + task.stop + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb new file mode 100644 index 0000000..27521b8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb @@ -0,0 +1,572 @@ +describe S2MessageHandler do + describe "S2::Messages::ReceptionStatus" do + it "forwards the message to the message sender" do + message_sender = instance_double(S2::MessageSender) + handler = described_class.new(message_sender:) + message = build(:s2_reception_status) + allow(message_sender).to receive(:receive_reception_status) + + handler.handle_message(message.to_json) + + expect(message_sender).to have_received(:receive_reception_status).with(message) + end + end + + describe "S2::Messages::Handshake" do + it "responds with ReceptionStatus OK when it's valid" do + message_sender = MessageSenderStub.new + state = { status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::Ok, + diagnostic_label: nil, + subject_message_id: message.message_id, + ) + end + + it "responds with a permanent error when no protocol version matches" do + message_sender = MessageSenderStub.new + state = { status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake, supported_protocol_versions: ["0.0.1"]) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: + "No protocol version matches, supported versions: #{S2.supported_protocol_versions.join(", ")}", + subject_message_id: message.message_id, + ) + end + + it "responds with a permanent error when the status is not websocket_connected" do + message_sender = MessageSenderStub.new + state = { status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: "Invalid state: expected status to be 'websocket_connected', got 'initialized'", + subject_message_id: message.message_id, + ) + end + + it "responds with a permanent error when the role is not CEM" do + message_sender = MessageSenderStub.new + state = { status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake, :rm) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: "Invalid attribute: expected role to be 'CEM', got 'RM'", + subject_message_id: message.message_id, + ) + end + end + + describe "S2::Messages::HandshakeResponse" do + it "responds with ReceptionStatus OK when it's valid" do + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with("GIT_REV", "unknown").and_return("unknown") + message_sender = MessageSenderStub.new + resource = create(:resource) + state = { resource_id: resource.s2_identifier, status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake_response) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(2) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::Ok, + diagnostic_label: nil, + subject_message_id: message.message_id, + ) + expect(message_sender.sent_messages.second).to have_attributes( + message_type: S2::Messages::ResourceManagerDetailsMessageType::ResourceManagerDetails, + manufacturer: "StekkerS2/unknown (test; Ruby/#{RUBY_VERSION}) S2/0.0.2-beta", + resource_id: state[:resource_id], + resource_manager_details_name: resource.configuration.fetch("name"), + roles: [ + have_attributes( + commodity: S2::Messages::Commodity::Electricity, + role: S2::Messages::RoleType::EnergyConsumer, + ), + ], + instruction_processing_delay: 5.minutes.in_milliseconds, + available_control_types: [S2::Messages::ControlType::FillRateBasedControl], + provides_forecast: true, + provides_power_measurement_types: [S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric], + ) + end + + it "updates the state to initialized and sets the protocol version when it's valid" do + message_sender = MessageSenderStub.new + resource = create(:resource) + state = { resource_id: resource.s2_identifier, status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake_response, selected_protocol_version: "0.0.2-beta") + + expect { handler.handle_message(message.to_json) } + .to change { state } + .from(resource_id: resource.s2_identifier, status: :websocket_connected) + .to(resource_id: resource.s2_identifier, status: :initialized, protocol_version: "0.0.2-beta") + end + + it "responds with a permanent error when the status is not websocket_connected" do + message_sender = MessageSenderStub.new + state = { resource_id: generate(:uuid), status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_handshake_response) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: "Invalid state: expected status to be 'websocket_connected', got 'initialized'", + subject_message_id: message.message_id, + ) + end + end + + describe "S2::Messages::SelectControlType" do + it "responds with ReceptionStatus OK when it's valid" do + message_sender = MessageSenderStub.new + state = { status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type, :frbc) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(2) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::Ok, + diagnostic_label: nil, + subject_message_id: message.message_id, + ) + end + + it "updates the state with the selected control type and FRBC system description when it's valid" do + message_sender = MessageSenderStub.new + resource = create(:resource) + state = { resource_id: resource.s2_identifier, status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type, :frbc) + + expect { handler.handle_message(message.to_json) } + .to change { state } + .from(resource_id: resource.s2_identifier, status: :initialized) + .to( + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description: be_a(S2::Messages::FRBCSystemDescription), + ) + end + + it "sends an FRBCSystemDescription request after selecting FillRateBasedControl" do + message_sender = MessageSenderStub.new + resource = create(:resource) + state = { resource_id: resource.s2_identifier, status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type, :frbc) + + freeze_time do + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(2) + expect(message_sender.sent_messages.second.to_dynamic).to match( + a_hash_including( + "message_type" => S2::Messages::FRBCSystemDescriptionMessageType::FRBCSystemDescription, + "valid_from" => Time.current.iso8601, + "actuators" => [ + a_hash_including( + "diagnostic_label" => "Electric Vehicle Charger", + "supported_commodities" => [S2::Messages::Commodity::Electricity], + "operation_modes" => [ + a_hash_including( + "id" => be_a(String), + "diagnostic_label" => "Off", + "elements" => [ + a_hash_including( + "fill_level_range" => a_hash_including( + "start_of_range" => 0, + "end_of_range" => 100, + ), + "fill_rate" => a_hash_including( + "start_of_range" => 0, + "end_of_range" => 0, + ), + "power_ranges" => [ + a_hash_including( + "start_of_range" => 0, + "end_of_range" => 0, + "commodity_quantity" => S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + ), + ], + ), + ], + "abnormal_condition_only" => false, + ), + a_hash_including( + "id" => be_a(String), + "diagnostic_label" => "Charging", + "elements" => [ + a_hash_including( + "fill_level_range" => a_hash_including( + "start_of_range" => 0, + "end_of_range" => 100, + ), + "fill_rate" => a_hash_including( + "start_of_range" => 0.0, + "end_of_range" => 0.00000001, + ), + "power_ranges" => [ + a_hash_including( + "start_of_range" => 0, + "end_of_range" => 11_040, + "commodity_quantity" => S2::Messages::CommodityQuantity::ElectricPower3_PhaseSymmetric, + ), + ], + ), + ], + ), + ], + "transitions" => all( + a_hash_including( + "from" => be_a(String), + "to" => be_a(String), + ), + ), + "timers" => [], + ), + ], + "storage" => a_hash_including( + "diagnostic_label" => "Electric Vehicle Battery", + "fill_level_label" => "State of Charge", + "provides_leakage_behaviour" => false, + "provides_fill_level_target_profile" => true, + "provides_usage_forecast" => false, + "fill_level_range" => a_hash_including( + "start_of_range" => -10, + "end_of_range" => 110, + ), + ), + ), + ) + end + end + + it "enqueues PublishS2ReadyNotificationJob when SelectControlType is handled" do + message_sender = MessageSenderStub.new + resource = create(:resource) + state = { resource_id: resource.s2_identifier, status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type, :frbc) + + expect { handler.handle_message(message.to_json) } + .to have_enqueued_job(PublishS2ReadyNotificationJob) + .with(resource_id: resource.s2_identifier) + end + + it "responds with a permanent error when the status is not initialized" do + message_sender = MessageSenderStub.new + state = { status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: "Invalid state: expected status to be 'initialized', got 'websocket_connected'", + subject_message_id: message.message_id, + ) + end + + it "responds with a permanent error when the control type is not FillRateBasedControl" do + message_sender = MessageSenderStub.new + state = { status: :initialized } + handler = described_class.new(message_sender:, state:) + message = build(:s2_select_control_type, :not_controlable) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: + "Invalid attribute: expected control_type to be 'FILL_RATE_BASED_CONTROL', got 'NOT_CONTROLABLE'", + subject_message_id: message.message_id, + ) + end + end + + describe "S2::Messages::FRBCInstruction" do + it "responds with ReceptionStatus OK when it's valid" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + duration: 5000, + ) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::Ok, + diagnostic_label: nil, + subject_message_id: message.message_id, + ) + end + + it "responds with invalid content error when the duration is zero" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + duration: 0, + ) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::InvalidContent, + diagnostic_label: "Invalid attribute: duration must be present and positive, got '0'", + subject_message_id: message.message_id, + ) + end + + it "responds with invalid content error when the duration is missing" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + ) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::InvalidContent, + diagnostic_label: "Invalid attribute: duration must be present and positive, got 'nil'", + subject_message_id: message.message_id, + ) + end + + it "does not schedule a job when the duration is invalid" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + duration: 0, + ) + + expect { handler.handle_message(message.to_json) } + .not_to have_enqueued_job(ProcessFRBCInstructionJob) + end + + it "schedules a ProcessFRBCInstructionJob" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + duration: 5000, + ) + + expect { handler.handle_message(message.to_json) } + .to have_enqueued_job(ProcessFRBCInstructionJob) + .with( + resource_id: resource.s2_identifier, + frbc_system_description_json: frbc_system_description.to_json, + frbc_instruction_json: message.to_json, + ) + end + + it "responds with a permanent error when the status is not initialized" do + message_sender = MessageSenderStub.new + state = { status: :websocket_connected } + handler = described_class.new(message_sender:, state:) + message = build(:s2_frbc_instruction) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: "Invalid state: expected status to be 'initialized', got 'websocket_connected'", + subject_message_id: message.message_id, + ) + end + + it "responds with a permanent error when the control type is not FillRateBasedControl" do + message_sender = MessageSenderStub.new + state = { status: :initialized, control_type: S2::Messages::ControlType::NotControlable } + handler = described_class.new(message_sender:, state:) + message = build(:s2_frbc_instruction) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::PermanentError, + diagnostic_label: + "Invalid state: expected control_type to be 'FILL_RATE_BASED_CONTROL', got 'NOT_CONTROLABLE'", + subject_message_id: message.message_id, + ) + end + + it "responds with invalid content error when the actuator_id is unknown" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: generate(:uuid), + operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, + ) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::InvalidContent, + diagnostic_label: "Invalid attribute: unknown actuator_id '#{message.actuator_id}'", + subject_message_id: message.message_id, + ) + end + + it "responds with invalid content error when the operation_mode is unknown" do + message_sender = MessageSenderStub.new + resource = create(:resource) + frbc_system_description = FRBCSystemDescriptionFactory.create(resource) + state = { + resource_id: resource.s2_identifier, + status: :initialized, + control_type: S2::Messages::ControlType::FillRateBasedControl, + frbc_system_description:, + } + handler = described_class.new(message_sender:, state:) + message = build( + :s2_frbc_instruction, + actuator_id: frbc_system_description.actuators.first.id, + operation_mode: generate(:uuid), + ) + + handler.handle_message(message.to_json) + + expect(message_sender.sent_messages.size).to eq(1) + expect(message_sender.sent_messages.first).to have_attributes( + message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + status: S2::Messages::ReceptionStatusValues::InvalidContent, + diagnostic_label: "Invalid attribute: unknown operation_mode '#{message.operation_mode}'", + subject_message_id: message.message_id, + ) + end + end + + describe "database connection isolation" do + it "handles each message inside the Rails executor so fibers don't share a connection" do + message_sender = instance_double(S2::MessageSender) + allow(message_sender).to receive(:receive_reception_status) + allow(Rails.application.executor).to receive(:wrap).and_call_original + + handler = described_class.new(message_sender:) + + handler.handle_message(build(:s2_reception_status).to_json) + + expect(Rails.application.executor).to have_received(:wrap) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb new file mode 100644 index 0000000..3d836a3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb @@ -0,0 +1,52 @@ +describe S2MessageLogger do + describe "#log_inbound" do + it "logs inbound S2 message with resource_id, message_type, and message" do + logger = instance_double(EcsLogging::Logger, info: nil) + message = '{"message_type": "FRBCInstruction", "id": "123"}' + + described_class.new(logger).log_inbound(resource_id: "ABC123", message:) + + expect(logger).to have_received(:info).with( + "Inbound S2 message", + s2: { + resource_id: "ABC123", + message_type: "FRBCInstruction", + message:, + }, + ) + end + + it "handles invalid JSON gracefully" do + logger = instance_double(EcsLogging::Logger, info: nil) + message = "not valid json" + + described_class.new(logger).log_inbound(resource_id: "ABC123", message:) + + expect(logger).to have_received(:info).with( + "Inbound S2 message", + s2: { + resource_id: "ABC123", + message:, + }, + ) + end + end + + describe "#log_outbound" do + it "logs outbound S2 message with resource_id, message_type, and message" do + logger = instance_double(EcsLogging::Logger, info: nil) + message = '{"message_type": "PowerMeasurement", "values": []}' + + described_class.new(logger).log_outbound(resource_id: "ABC123", message:) + + expect(logger).to have_received(:info).with( + "Outbound S2 message", + s2: { + resource_id: "ABC123", + message_type: "PowerMeasurement", + message:, + }, + ) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb new file mode 100644 index 0000000..5981334 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb @@ -0,0 +1,472 @@ +describe Supervisor, :async do + before do + described_class.instance.shutdown + + mqtt_listener = instance_double(Stekker::MQTT::Listener) + allow(mqtt_listener).to receive(:start) + allow(mqtt_listener).to receive(:stop) + allow(Stekker::MQTT::Listener).to receive(:new).and_return(mqtt_listener) + end + + after do + described_class.instance.shutdown + end + + describe "#start_all" do + it "sets the status to :running" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(supervisor.status).to eq(:running) + end + + it "does not start if already running" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + expect(supervisor.status).to eq(:running) + + supervisor.startup + Async::Task.current.sleep 0.01 + expect(supervisor.status).to eq(:running) + end + + it "starts sessions for each active resource" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + active_resource = create(:resource, :active) + inactive_resource = create(:resource, :inactive) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(supervisor.find_connection(active_resource.s2_identifier).status).to eq(:connected) + expect(supervisor.find_connection(inactive_resource.s2_identifier)).to be_nil + end + + it "sends a handshake message upon starting a session" do + ws = FakeWebSocket.new + allow(Async::WebSocket::Client).to receive(:connect).and_yield(ws) + _resource = create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(ws.sent_messages).to include(a_string_matching(/"message_type":"Handshake"/)) + end + + it "starts the MQTT service" do + mqtt_listener = instance_double(Stekker::MQTT::Listener) + allow(mqtt_listener).to receive(:start) + allow(mqtt_listener).to receive(:stop) + allow(Stekker::MQTT::Listener).to receive(:new).and_return(mqtt_listener) + supervisor = described_class.instance + + supervisor.startup + sleep 0.01 + supervisor.shutdown + + expect(mqtt_listener).to have_received(:start).once + end + end + + describe "#stop_all" do + it "sets the status to :stopped" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + expect(supervisor.status).to eq(:running) + + supervisor.shutdown + Async::Task.current.sleep 0.01 + expect(supervisor.status).to eq(:stopped) + end + + it "stops all sessions" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + expect(supervisor.find_connection(resource.s2_identifier).status).to eq(:connected) + + supervisor.shutdown + Async::Task.current.sleep 0.01 + expect(supervisor.find_connection(resource.s2_identifier)).to be_nil + end + end + + describe "#send_message" do + it "sends a message to a specific session" do + ws = FakeWebSocket.new + allow(Async::WebSocket::Client).to receive(:connect).and_yield(ws) + resource = create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + expect(supervisor.find_connection(resource.s2_identifier).status).to eq(:connected) + + message = { type: "test_message" } + supervisor.send_message(resource.s2_identifier, message) + Async::Task.current.sleep 0.01 + + expect(ws.sent_messages).to include(message.to_json) + end + end + + describe "#running?" do + it "returns true when running" do + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(supervisor.running?).to be true + end + + it "returns false when stopped" do + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + supervisor.shutdown + Async::Task.current.sleep 0.01 + + expect(supervisor.running?).to be false + end + end + + describe "#connect" do + it "passes basic auth headers when CEM auth is configured" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + + allow(ReferenceConfig).to receive(:cem).and_return( + ReferenceConfig::Cem.new(base_url: "ws://example.test/s2", username: "user", password: "pass"), + ) + + create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(Async::WebSocket::Client).to have_received(:connect).with( + anything, + headers: { "authorization" => "Basic #{Base64.strict_encode64("user:pass")}" }, + ) + end + + it "passes no auth headers when CEM auth is not configured" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + + allow(ReferenceConfig).to receive(:cem).and_return( + ReferenceConfig::Cem.new(base_url: "ws://example.test/s2", username: nil, password: nil), + ) + + create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(Async::WebSocket::Client).to have_received(:connect).with( + anything, + headers: {}, + ) + end + + it "passes no auth headers when only username is configured" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + + allow(ReferenceConfig).to receive(:cem).and_return( + ReferenceConfig::Cem.new(base_url: "ws://example.test/s2", username: "user", password: nil), + ) + + create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(Async::WebSocket::Client).to have_received(:connect).with( + anything, + headers: {}, + ) + end + + it "passes no auth headers when only password is configured" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + + allow(ReferenceConfig).to receive(:cem).and_return( + ReferenceConfig::Cem.new(base_url: "ws://example.test/s2", username: nil, password: "pass"), + ) + + create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(Async::WebSocket::Client).to have_received(:connect).with( + anything, + headers: {}, + ) + end + end + + describe "#connection" do + it "returns the connection for a resource" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + expect(supervisor.find_connection(resource.s2_identifier)).to be_nil + + supervisor.startup + Async::Task.current.sleep 0.01 + + expect(supervisor.find_connection(resource.s2_identifier).status).to eq(:connected) + expect(supervisor.find_connection(resource.s2_identifier).status).to eq(:connected) + + supervisor.shutdown + Async::Task.current.sleep 0.01 + + expect(supervisor.find_connection(resource.s2_identifier)).to be_nil + end + end + + describe "reconnect on unexpected disconnection" do + def fire_connection_errored(resource_id) + exception = StandardError.new("ws went away").tap { |e| e.set_backtrace(caller) } + ActiveSupport::Notifications.instrument( + "connection_errored.session.s2", + resource_id:, + exception:, + ) + end + + def fire_connected(resource_id) + ActiveSupport::Notifications.instrument( + "connected.session.s2", + resource_id:, + url: "ws://example.com/#{resource_id}", + ) + end + + it "reschedules the connection after the current backoff when it errors" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + sleep_durations = [] + allow(supervisor).to receive(:sleep) { |duration| sleep_durations << duration } + allow(supervisor).to receive(:reconnect).and_call_original + + supervisor.startup + Async::Task.current.sleep 0.01 + + allow(supervisor.find_connection(resource.s2_identifier)).to receive(:status).and_return(:reconnecting) + + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + + expect(sleep_durations).to eq([5.0]) + expect(supervisor).to have_received(:reconnect).with(resource.s2_identifier) + end + + it "does not reconnect when the connection has already recovered before the backoff elapsed" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + allow(supervisor).to receive(:sleep) + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + + expect(supervisor).not_to have_received(:reconnect) + end + + it "doubles the backoff on consecutive errors up to five minutes" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + sleep_durations = [] + allow(supervisor).to receive(:sleep) { |duration| sleep_durations << duration } + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + + 15.times do + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + end + + expect(sleep_durations).to eq( + [5.0, 10.0, 20.0, 40.0, 80.0, 160.0, 300.0, 300.0, 300.0, 300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + ) + end + + it "resets the backoff when the connection reports connected again" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + sleep_durations = [] + allow(supervisor).to receive(:sleep) { |duration| sleep_durations << duration } + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + + fire_connected(resource.s2_identifier) + + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + + expect(sleep_durations).to eq([5.0, 10.0, 5.0]) + end + + it "does not reconnect resources that are no longer active" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + inactive = create(:resource, :inactive) + supervisor = described_class.instance + + allow(supervisor).to receive(:sleep) + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + + fire_connection_errored(inactive.s2_identifier) + Async::Task.current.sleep 0.01 + + expect(supervisor).not_to have_received(:reconnect) + end + + it "coalesces rapid errors so only one reconnect is scheduled at a time" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + sleep_durations = [] + sleep_started = Async::Notification.new + release_sleep = Async::Notification.new + allow(supervisor).to receive(:sleep) do |duration| + sleep_durations << duration + sleep_started.signal + release_sleep.wait + end + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + + allow(supervisor.find_connection(resource.s2_identifier)).to receive(:status).and_return(:reconnecting) + + fire_connection_errored(resource.s2_identifier) + sleep_started.wait + + 2.times { fire_connection_errored(resource.s2_identifier) } + release_sleep.signal + Async::Task.current.sleep 0.01 + + expect(sleep_durations).to eq([5.0]) + expect(supervisor).to have_received(:reconnect).once + end + + it "stops listening after shutdown" do + allow(Async::WebSocket::Client).to receive(:connect).and_yield(FakeWebSocket.new) + resource = create(:resource) + supervisor = described_class.instance + + allow(supervisor).to receive(:reconnect) + + supervisor.startup + Async::Task.current.sleep 0.01 + supervisor.shutdown + Async::Task.current.sleep 0.01 + + fire_connection_errored(resource.s2_identifier) + Async::Task.current.sleep 0.01 + + expect(supervisor).not_to have_received(:reconnect) + end + end + + describe "integration tests", :ws_server do + it "connects a websocket client to a websocket server" do + endpoint = Async::HTTP::Endpoint.parse("#{ws_server.url}/ws") + + Async do + Async::WebSocket::Client.connect(endpoint) do |ws| + ws.write("abc") + expect(ws.read.to_str).to eq("abc") + end + end.wait + end + + it "supervisor connects to the websocket server" do + sent_messages = [] + received_messages = [] + + message_sent_subscriber = ActiveSupport::Notifications.subscribe("message_sent.session.s2") do |event| + sent_messages << event.payload[:payload] + end + + message_rcvd_subscriber = ActiveSupport::Notifications.subscribe("message_received.session.s2") do |event| + received_messages << event.payload[:payload] + end + + create(:resource) + supervisor = described_class.instance + + supervisor.startup + Async::Task.current.sleep 0.1 + + expect(supervisor.status).to eq(:running) + expect(supervisor.find_connection(Resource.first.s2_identifier).status).to eq(:connected) + + expect(sent_messages.size).to eq(2) + expect(sent_messages.first).to include( + %("message_type":"Handshake","role":"RM","supported_protocol_versions":["0.0.2-beta"]), + ) + expect(sent_messages.second).to include( + %("diagnostic_label":"Invalid attribute: expected role to be 'CEM', got 'RM'","message_type":"ReceptionStatus","status":"PERMANENT_ERROR"), # rubocop:disable Layout/LineLength + ) + + # Because the WebsocketServer merely echoes received messages + expect(received_messages.size).to eq(2) + expect(received_messages.first).to include( + %("message_type":"Handshake","role":"RM","supported_protocol_versions":["0.0.2-beta"]), + ) + expect(received_messages.second).to include( + %("diagnostic_label":"Invalid attribute: expected role to be 'CEM', got 'RM'","message_type":"ReceptionStatus","status":"PERMANENT_ERROR"), # rubocop:disable Layout/LineLength + ) + ensure + ActiveSupport::Notifications.unsubscribe(message_sent_subscriber) + ActiveSupport::Notifications.unsubscribe(message_rcvd_subscriber) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb new file mode 100644 index 0000000..128dc12 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe TrafficLog do + let(:dir) { Dir.mktmpdir } + + before { stub_const("TrafficLog::PATH", Pathname.new(dir).join("traffic.jsonl")) } + after { FileUtils.remove_entry(dir) } + + describe ".clear" do + it "drops the whole log so a demo restart starts from a clean slate" do + described_class.record(peer: "cem", direction: "out", message_type: "Handshake") + described_class.record(peer: "cem", direction: "in", message_type: "SelectControlType") + described_class.record(peer: "mqtt", direction: "in", message_type: "power_measurement") + described_class.record(peer: "cem", direction: "in", message_type: "FRBC.Instruction") + + described_class.clear + + expect(described_class.recent).to eq([]) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb new file mode 100644 index 0000000..85ca06e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb @@ -0,0 +1,22 @@ +describe Resource do + describe "validations" do + subject { build(:resource) } + + it { is_expected.to validate_presence_of(:s2_identifier) } + end + + describe "scopes" do + describe "::active" do + it "returns active resources" do + active_resource = create(:resource, :active) + _inactive_resource = create(:resource, :inactive) + + expect(described_class.active).to eq([active_resource]) + end + + it "returns an empty list when no resources are active" do + expect(described_class.active).to eq([]) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb new file mode 100644 index 0000000..7595049 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb @@ -0,0 +1,34 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "rspec/rails" + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +Rails.root.glob("spec/support/**/*.rb").each { |f| require f } + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.fixture_paths = [ + Rails.root.join("spec/fixtures"), + ] + + config.include ActiveSupport::Testing::TimeHelpers + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.example_status_persistence_file_path = ".rspec-failures" + config.filter_rails_from_backtrace! + config.infer_spec_type_from_file_location! + config.shared_context_metadata_behavior = :apply_to_host_groups + config.use_transactional_fixtures = true +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb new file mode 100644 index 0000000..a239b27 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb @@ -0,0 +1,48 @@ +describe "Resources" do + describe "GET /resources/:id" do + it "returns connection state as JSON" do + resource = create(:resource) + connection_state = { + resource_id: resource.s2_identifier, + status: :initialized, + frbc_system_description: { + actuators: [{ id: "actuator-1", operation_modes: [{ id: "mode-1" }] }], + }, + } + connection = instance_double(S2::Connection, state: connection_state) + supervisor = instance_double(Supervisor, find_connection: connection) + allow(Supervisor).to receive(:instance).and_return(supervisor) + + get resource_path(resource, format: :json) + + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + + expect(response.parsed_body["resource_id"]).to eq(resource.s2_identifier) + expect(response.parsed_body.dig("frbc_system_description", "actuators", 0, "id")).to eq("actuator-1") + end + + it "returns empty JSON when no connection exists" do + resource = create(:resource) + supervisor = instance_double(Supervisor, find_connection: nil) + allow(Supervisor).to receive(:instance).and_return(supervisor) + + get resource_path(resource, format: :json) + + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq({}) + end + + it "renders HTML with N/A uptime when the connection has not reached :connected yet" do + resource = create(:resource) + connection = instance_double(S2::Connection, connected_at: nil, state: nil, status: :connecting) + supervisor = instance_double(Supervisor, find_connection: connection) + allow(Supervisor).to receive(:instance).and_return(supervisor) + + get resource_path(resource) + + expect(response).to have_http_status(:ok) + expect(response.body).to include("N/A") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb new file mode 100644 index 0000000..b441362 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb @@ -0,0 +1,22 @@ +describe "Traffic" do + describe "POST /traffic/reset" do + it "is not routable without the demo screen switch" do + post "/traffic/reset" + + expect(response).to have_http_status(:not_found) + end + + it "clears the feed when the demo screen switch is on" do + ENV["DEMO_ALLOW_FRAMING"] = "1" + Rails.application.reload_routes! + + post "/traffic/reset" + + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq("reset" => true) + ensure + ENV.delete("DEMO_ALLOW_FRAMING") + Rails.application.reload_routes! + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb new file mode 100644 index 0000000..7efbad3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb @@ -0,0 +1,5 @@ +describe StekkerS2::Application do + it "isolates ActiveRecord per fiber so the Async reactor's fibers don't share a connection" do + expect(ActiveSupport::IsolatedExecutionState.isolation_level).to eq(:fiber) + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb new file mode 100644 index 0000000..9c5bfa0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb @@ -0,0 +1,188 @@ +describe LogMQTTListenerSubscriber do + describe "starting" do + it "logs the starting event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "starting.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + {}, + ) + + described_class.new.starting(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] Starting MQTT listener") + end + end + + describe "connected" do + it "logs the connected event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "connected.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + {}, + ) + + described_class.new.connected(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] MQTT listener connected") + end + end + + describe "subscribed" do + it "logs the subscribed event with topics" do + allow(Rails.logger).to receive(:info) + topics = %w[topic1 topic2] + event = ActiveSupport::Notifications::Event.new( + "subscribed.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + { topics: }, + ) + + described_class.new.subscribed(event) + + expect(Rails.logger) + .to have_received(:info).with("[#{described_class}] Subscribed to MQTT topics: [\"topic1\", \"topic2\"]") + end + end + + describe "message_received" do + it "logs the message received event via MQTTMessageLogger" do + ecs_logger = instance_double(EcsLogging::Logger, info: nil) + allow(Rails.configuration).to receive(:ecs_logger).and_return(ecs_logger) + + topic = "stekker/resources/ABC123/measurements" + message = '{"state": "charging"}' + event = ActiveSupport::Notifications::Event.new( + "message_received.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + { topic:, message: }, + ) + + described_class.new.message_received(event) + + expect(ecs_logger).to have_received(:info).with( + "Inbound MQTT message", + mqtt: { + topic:, + resource_id: "ABC123", + message:, + }, + ) + end + end + + describe "listen_errored" do + it "notifies error when listen_errored event occurs" do + allow(ErrorNotifier).to receive(:notify_error) + exception = StandardError.new("Test error") + event = ActiveSupport::Notifications::Event.new( + "listen_errored.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + { exception: }, + ) + + described_class.new.listen_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with(exception) + end + end + + describe "connection_errored" do + it "notifies error when connection_errored event occurs" do + allow(ErrorNotifier).to receive(:notify_error) + exception = StandardError.new("Test error") + event = ActiveSupport::Notifications::Event.new( + "connection_errored.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + { exception: }, + ) + + described_class.new.connection_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with(exception) + end + end + + describe "get_errored" do + it "notifies error when get_errored event occurs" do + allow(ErrorNotifier).to receive(:notify_error) + exception = StandardError.new("Test error") + event = ActiveSupport::Notifications::Event.new( + "get_errored.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + { exception: }, + ) + + described_class.new.get_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with(exception) + end + end + + describe "graceful_shutdown" do + it "logs the graceful shutdown event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "graceful_shutdown.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + {}, + ) + + described_class.new.graceful_shutdown(event) + + expect(Rails.logger) + .to have_received(:info).with("[#{described_class}] MQTT listener shutting down gracefully") + end + end + + describe "thread_exited" do + it "logs the thread exited event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "thread_exited.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + {}, + ) + + described_class.new.thread_exited(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] MQTT listener thread exited") + end + end + + describe "stopped" do + it "logs the stopped event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "stopped.listener.mqtt.stekker", + Time.current, + Time.current, + 1, + {}, + ) + + described_class.new.stopped(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] MQTT listener stopped") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb new file mode 100644 index 0000000..cdb9579 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb @@ -0,0 +1,20 @@ +describe LogMQTTMessageHandlerSubscriber do + describe "#message_errored" do + it "notifies the error notifier with the exception from the event payload" do + allow(ErrorNotifier).to receive(:notify_error) + exception = StandardError.new("Test error") + event = ActiveSupport::Notifications::Event.new( + "message_errored.message_handler.mqtt.stekker", + Time.current, + Time.current, + SecureRandom.uuid, + { exception: }, + ) + + subscriber = described_class.new + subscriber.message_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with(exception) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb new file mode 100644 index 0000000..aa7eec3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb @@ -0,0 +1,210 @@ +describe LogSessionSubscriber do + describe "connected" do + it "logs the connection event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "connected.session.s2", + Time.current, + Time.current, + 1, + { resource_id: 123, url: "ws://example.com" }, + ) + + described_class.new.connected(event) + + expect(Rails.logger) + .to have_received(:info).with("[#{described_class}] [123] S2 Session connected to ws://example.com") + end + end + + describe "started" do + it "logs the started event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "started.session.s2", + Time.current, + Time.current, + 1, + { resource_id: 123 }, + ) + + described_class.new.started(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] [123] S2 Session started") + end + end + + describe "disconnected" do + it "logs the disconnected event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "disconnected.session.s2", + Time.current, + Time.current, + 1, + { resource_id: 123 }, + ) + + described_class.new.disconnected(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] [123] S2 Session disconnected") + end + end + + describe "stopped" do + it "logs the stopped event" do + allow(Rails.logger).to receive(:info) + event = ActiveSupport::Notifications::Event.new( + "stopped.session.s2", + Time.current, + Time.current, + 1, + { resource_id: 123 }, + ) + + described_class.new.stopped(event) + + expect(Rails.logger).to have_received(:info).with("[#{described_class}] [123] S2 Session stopped") + end + end + + describe "message_received" do + it "logs the message received event via S2MessageLogger" do + ecs_logger = instance_double(EcsLogging::Logger, info: nil) + allow(Rails.configuration).to receive(:ecs_logger).and_return(ecs_logger) + + message = '{"message_type": "FRBCInstruction", "id": "123"}' + event = ActiveSupport::Notifications::Event.new( + "message_received.session.s2", + Time.current, + Time.current, + 1, + { resource_id: "ABC123", payload: message }, + ) + + described_class.new.message_received(event) + + expect(ecs_logger).to have_received(:info).with( + "Inbound S2 message", + s2: { + resource_id: "ABC123", + message_type: "FRBCInstruction", + message:, + }, + ) + end + end + + describe "message_sent" do + it "logs the message sent event via S2MessageLogger" do + ecs_logger = instance_double(EcsLogging::Logger, info: nil) + allow(Rails.configuration).to receive(:ecs_logger).and_return(ecs_logger) + + message = '{"message_type": "PowerMeasurement", "values": []}' + event = ActiveSupport::Notifications::Event.new( + "message_sent.session.s2", + Time.current, + Time.current, + 1, + { resource_id: "ABC123", payload: message }, + ) + + described_class.new.message_sent(event) + + expect(ecs_logger).to have_received(:info).with( + "Outbound S2 message", + s2: { + resource_id: "ABC123", + message_type: "PowerMeasurement", + message:, + }, + ) + end + end + + describe "connection_errored" do + before { allow(ErrorNotifier).to receive(:notify_error) } + + it "notifies the error and tags it with the resource_id" do + exception = StandardError.new("Test error") + event = ActiveSupport::Notifications::Event.new( + "connection_errored.session.s2", + Time.current, + Time.current, + 1, + { exception:, resource_id: "RM-123" }, + ) + + described_class.new.connection_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with( + exception, + extra: {}, + tags: { resource_id: "RM-123" }, + ) + end + + it "attaches HTTP response details from Async::WebSocket::ConnectionError" do + body = instance_double(Protocol::HTTP::Body::Readable, read: "Resource not found") + headers = Protocol::HTTP::Headers["content-type" => "application/problem+json"] + response = Protocol::HTTP::Response.new("HTTP/2", 404, headers, body) + exception = Async::WebSocket::ConnectionError.new("Failed to negotiate connection!", response) + event = ActiveSupport::Notifications::Event.new( + "connection_errored.session.s2", + Time.current, + Time.current, + 1, + { exception:, resource_id: "RM-123" }, + ) + + described_class.new.connection_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with( + exception, + extra: { + http_status: 404, + http_headers: { "content-type" => "application/problem+json" }, + http_body: "Resource not found", + }, + tags: { resource_id: "RM-123" }, + ) + end + + it "omits HTTP details when the ConnectionError carries no response" do + exception = Async::WebSocket::ConnectionError.new("Failed to negotiate connection!", nil) + event = ActiveSupport::Notifications::Event.new( + "connection_errored.session.s2", + Time.current, + Time.current, + 1, + { exception:, resource_id: "RM-123" }, + ) + + described_class.new.connection_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with( + exception, + extra: {}, + tags: { resource_id: "RM-123" }, + ) + end + end + + describe "message_errored" do + it "notifies the error" do + exception = StandardError.new("Test error") + allow(ErrorNotifier).to receive(:notify_error) + event = ActiveSupport::Notifications::Event.new( + "message_errored.session.s2", + Time.current, + Time.current, + 1, + { exception: }, + ) + + described_class.new.message_errored(event) + + expect(ErrorNotifier).to have_received(:notify_error).with(exception) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb new file mode 100644 index 0000000..1f8a646 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb @@ -0,0 +1,10 @@ +require "async" + +RSpec.configure do |config| + config.around(:each, :async) do |example| + Async do |task| + example.run + task.stop + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb new file mode 100644 index 0000000..3b70d43 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb @@ -0,0 +1,12 @@ +require "stekker/mqtt/fake_client" +require "stekker/mqtt/rspec/matchers" + +def with_fake_mqtt_client(&) # rubocop:disable Metrics/AbcSize + fake_mqtt_client = Stekker::MQTT::FakeClient.new + allow(MQTT::Client).to receive(:connect).and_yield(fake_mqtt_client) + + yield fake_mqtt_client + + expect(MQTT::Client).to have_received(:connect).at_least(:once).with(ReferenceConfig.mqtt_url) + allow(MQTT::Client).to receive(:connect).and_call_original +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb new file mode 100644 index 0000000..3f6bbf2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb @@ -0,0 +1,40 @@ +class FakeWebSocket + attr_reader :sent_messages + + def initialize + @incoming = Async::Queue.new + @buffered_messages = [] + @sent_messages = [] + @closed = false + end + + def read + return nil if @closed + + @incoming.dequeue + rescue Async::Stop + nil + end + + def write(payload) + @buffered_messages << payload + end + + def flush + @sent_messages.concat(@buffered_messages) + @buffered_messages.clear + end + + def close + @closed = true + @incoming.enqueue(nil) # Unblock any waiting reads + end + + def simulate_incoming(payload) + @incoming.enqueue(payload) + end + + def closed? + @closed + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb new file mode 100644 index 0000000..9cbde6d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb @@ -0,0 +1,17 @@ +class MessageSenderStub < S2::MessageSender + attr_reader :sent_messages + + def initialize + super(resource_id: FactoryBot.generate(:uuid)) do |_message| + # ignored + end + + @sent_messages = [] + end + + def send_message(message, &) + @sent_messages << message + + yield if block_given? + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb new file mode 100644 index 0000000..7d045f3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb @@ -0,0 +1,6 @@ +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb b/resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb new file mode 100644 index 0000000..e5edef0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb @@ -0,0 +1,87 @@ +require "async/websocket/adapters/http" + +class WebsocketServer + DEFAULT_PATH = "/ws".freeze + + attr_reader :url, :port + + def initialize(host: "127.0.0.1", path: DEFAULT_PATH) + @host = host + @path = path + @port = find_free_port(host) + @endpoint = Async::HTTP::Endpoint.parse( + "http://#{@host}:#{@port}", + protocol: Async::HTTP::Protocol::HTTP1, + ) + @server = Async::HTTP::Server.new(method(:app), @endpoint) + @url = "http://#{@host}:#{@port}" + @reactor = @task = nil + end + + def start + @reactor = Async do |task| + @task = task.async { @server.run } + @task.sleep 0.01 + end + + self + end + + def stop + @task&.stop + @reactor&.stop + ensure + @task = @reactor = nil + end + + private + + def app(request) # rubocop:disable Metrics/AbcSize + Rails.logger.info "WebSocket Server received request: #{request.method} #{request.path}" + + if request.path.start_with?(@path) && Async::WebSocket::Adapters::HTTP.websocket?(request) + Rails.logger.info "WebSocket Server upgrading to WebSocket protocol" + + response = Async::WebSocket::Adapters::HTTP.open(request) do |ws| + Rails.logger.info "WebSocket Server connected" + + while (msg = ws.read) + Rails.logger.info "WebSocket Server received: #{msg.to_str}" + ws.write(msg) # echo message back + end + end + + return response + end + + Async::HTTP::Protocol::Response[200, { "content-type" => "text/plain" }, ["OK"]] + end + + def find_free_port(host) + TCPServer.open(host, 0) { |s| s.addr[1] } + end +end + +module WebsocketServerHelper + def ws_server + @ws_server + end +end + +RSpec.configure do |config| + config.include WebsocketServerHelper + + config.around(:each, :ws_server) do |example| + old_cem_base_url = ENV["CEM_BASE_URL"] + @ws_server = WebsocketServer.new.start + ENV["CEM_BASE_URL"] = "#{@ws_server.url}/ws" + + begin + example.run + ensure + @ws_server.stop + @ws_server = nil + ENV["CEM_BASE_URL"] = old_cem_base_url + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/.keep b/resource-manager-mqtt-bridge-ruby/rm/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore new file mode 100644 index 0000000..71aca8e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +spec/examples.txt diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version new file mode 100644 index 0000000..47b322c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile new file mode 100644 index 0000000..fddcede --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in stekker-mqtt.gemspec +gemspec + +gem "factory_bot" + +gem "rake", "~> 13.0" + +gem "rspec" diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock new file mode 100644 index 0000000..42d4ad7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock @@ -0,0 +1,95 @@ +PATH + remote: . + specs: + stekker-mqtt (0.1.0) + activesupport (>= 6.0) + dry-struct + dry-types + mqtt + +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.4.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + diff-lcs (1.6.2) + drb (2.2.3) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-struct (1.8.0) + dry-core (~> 1.1) + dry-types (~> 1.8, >= 1.8.2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.9.1) + bigdecimal (>= 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + factory_bot (6.5.5) + activesupport (>= 6.1.0) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + logger (1.7.0) + minitest (5.27.0) + mqtt (0.7.0) + logger + rake (13.3.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.6) + securerandom (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + zeitwerk (2.7.3) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + factory_bot + rake (~> 13.0) + rspec + stekker-mqtt! + +BUNDLED WITH + 2.6.9 diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt new file mode 100644 index 0000000..b0cb2e3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Stekker.app B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall 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. diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md new file mode 100644 index 0000000..fb89ae4 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md @@ -0,0 +1,3 @@ +# Stekker::MQTT + +Contains MQTT message definitions for Stekker. \ No newline at end of file diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console new file mode 100755 index 0000000..17c33ca --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "stekker/mqtt" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages new file mode 100755 index 0000000..34bb476 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +require "json" + +Dir.glob("lib/json-schema/*.schema.json").each do |file| + json = JSON.parse(File.read(file)) + json["type"] ||= "object" unless json["enum"] + File.write(file, JSON.pretty_generate(json, indent: " ") + "\n") +end + +`npx -y quicktype -s schema lib/json-schema/*.schema.json -l ruby --namespace Stekker::MQTT::Messages -o lib/stekker/mqtt/messages.rb --strictness coercible` diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json new file mode 100644 index 0000000..fae6e8f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/charge-status.schema.json", + "title": "ChargeStatus", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "charge_status", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "sampled_at", + "value" + ], + "properties": { + "sampled_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Timestamp when the measurement was taken." + }, + "value": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Percentage", + "description": "Charge status as a percentage." + } + }, + "description": "Container for charge status." + } + }, + "examples": [ + { + "message_type": "charge_status", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "sampled_at": "2025-09-29T12:34:56Z", + "value": 57.8 + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json new file mode 100644 index 0000000..f6b5dea --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/charge-target.schema.json", + "title": "ChargeTarget", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "charge_target", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "starts_at", + "ends_at", + "direct_percentage", + "smart_percentage" + ], + "properties": { + "starts_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Timestamp when the charge target starts." + }, + "ends_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Timestamp when the charge target ends." + }, + "direct_percentage": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Percentage", + "description": "The charge level to be reached as soon as possible." + }, + "smart_percentage": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Percentage", + "description": "The charge level to be reached before ends_at." + } + }, + "description": "Container for charge target." + } + }, + "examples": [ + { + "message_type": "charge_target", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "sampled_at": "2025-09-29T12:34:56Z", + "value": 57.8 + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json new file mode 100644 index 0000000..6a9dfdb --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/common.json", + "title": "CommonDefs", + "$defs": { + "Percentage": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Percentage value between 0 and 100." + }, + "Timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC recommended)." + }, + "UUIDv4": { + "type": "string", + "format": "uuid", + "description": "UUID v4 or v5 identifier.", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[45][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" + }, + "UnitW": { + "type": "string", + "const": "W", + "description": "Unit of measurement. Always 'W'." + } + }, + "type": "object" +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json new file mode 100644 index 0000000..f07e5dd --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/connect.schema.json", + "title": "Connect", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "connect", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "voltage", + "number_of_phases", + "minimum_current", + "maximum_current" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name of the connector or resource." + }, + "voltage": { + "type": "integer", + "minimum": 1, + "description": "Nominal voltage of the connection in volts." + }, + "number_of_phases": { + "type": "integer", + "enum": [ + 1, + 3 + ], + "description": "Number of electrical phases available. Must be 1 or 3." + }, + "minimum_current": { + "type": "integer", + "minimum": 0, + "description": "Minimum available current per phase in amperes." + }, + "maximum_current": { + "type": "integer", + "minimum": 1, + "description": "Maximum available current per phase in amperes." + } + }, + "description": "Container object with connection details." + } + }, + "examples": [ + { + "message_type": "connect", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "name": "Some name", + "voltage": 230, + "number_of_phases": 3, + "minimum_current": 6, + "maximum_current": 16 + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json new file mode 100644 index 0000000..af4a71f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/disconnect.schema.json", + "title": "Disconnect", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id" + ], + "properties": { + "message_type": { + "const": "disconnect", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + } + }, + "examples": [ + { + "message_type": "disconnect", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json new file mode 100644 index 0000000..cc56b3c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/instruction-update.schema.json", + "title": "InstructionUpdate", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "instruction_update", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "reference", + "status", + "updated_at" + ], + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "description": "Reference string identifying the instruction to update." + }, + "status": { + "type": "string", + "enum": [ + "NEW", + "ACCEPTED", + "REJECTED", + "REVOKED", + "STARTED", + "SUCCEEDED", + "ABORTED" + ], + "description": "Instruction status. Must be one of: REJECTED, STARTED, SUCCEEDED, ABORTED." + }, + "updated_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Timestamp when the instruction status was last updated." + } + }, + "description": "Instruction update details." + } + }, + "examples": [ + { + "message_type": "instruction_update", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "reference": "abc-123", + "status": "SUCCEEDED", + "updated_at": "2025-10-06T14:25:00Z" + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json new file mode 100644 index 0000000..41a65a2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/power-forecast.schema.json", + "title": "PowerForecast", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "power_forecast", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "start_at", + "values", + "unit" + ], + "properties": { + "start_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Start time of the forecast (ISO 8601)." + }, + "values": { + "type": "array", + "minItems": 1, + "description": "List of forecasted power values over time.", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "timestamp", + "value" + ], + "properties": { + "timestamp": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp" + }, + "value": { + "type": "integer", + "minimum": 0, + "description": "Forecasted power value in watts." + } + } + } + }, + "unit": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UnitW" + } + }, + "description": "Container for forecast data." + } + }, + "examples": [ + { + "message_type": "power_forecast", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "start_at": "2025-09-29T12:30:00Z", + "values": [ + { + "timestamp": "2025-09-29T12:30:00Z", + "value": 3500 + }, + { + "timestamp": "2025-09-29T12:45:00Z", + "value": 4000 + } + ], + "unit": "W" + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json new file mode 100644 index 0000000..3738650 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/power-measurement.schema.json", + "title": "PowerMeasurement", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "power_measurement", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "sampled_at", + "value", + "unit" + ], + "properties": { + "sampled_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Timestamp when the measurement was taken." + }, + "value": { + "type": "integer", + "minimum": 0, + "description": "Power value in watts." + }, + "unit": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UnitW" + } + }, + "description": "Container for measurement data." + } + }, + "examples": [ + { + "message_type": "power_measurement", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "sampled_at": "2025-09-29T12:34:56Z", + "value": 7200, + "unit": "W" + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json new file mode 100644 index 0000000..cc1457f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/power-setpoint.schema.json", + "title": "PowerSetpoint", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id", + "data" + ], + "properties": { + "message_type": { + "const": "power_setpoint", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "reference", + "starts_at", + "value", + "unit" + ], + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "description": "Unique reference string for this power setpoint instruction." + }, + "starts_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "Start time of the power setpoint in ISO 8601 format." + }, + "ends_at": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/Timestamp", + "description": "End time of the power setpoint in ISO 8601 format." + }, + "value": { + "type": "integer", + "minimum": 0, + "description": "Maximum allowed power in watts." + }, + "unit": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UnitW" + } + }, + "description": "Container object with power setpoint details." + } + }, + "examples": [ + { + "message_type": "power_setpoint", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "data": { + "reference": "abc-123", + "starts_at": "2025-10-06T12:00:00Z", + "ends_at": "2025-10-06T13:00:00Z", + "value": 11040, + "unit": "W" + } + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json new file mode 100644 index 0000000..2187034 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stekker.app/s2_ready_notification.schema.json", + "title": "S2ReadyNotification", + "type": "object", + "additionalProperties": false, + "required": [ + "message_type", + "resource_id" + ], + "properties": { + "message_type": { + "const": "s2_ready_notification", + "description": "Fixed message type identifier." + }, + "resource_id": { + "$ref": "https://schemas.stekker.app/common.json#/$defs/UUIDv4" + } + }, + "examples": [ + { + "message_type": "s2_ready_notification", + "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ] +} diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb new file mode 100644 index 0000000..649c9b4 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb @@ -0,0 +1,14 @@ +require_relative "mqtt/factories" +require_relative "mqtt/listener" +require_relative "mqtt/message_factory" +require_relative "mqtt/message_handler" +require_relative "mqtt/messages" +require_relative "mqtt/publisher" +require_relative "mqtt/version" + +module Stekker + module MQTT + class Error < StandardError; end + # Your code goes here... + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb new file mode 100644 index 0000000..8c46feb --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb @@ -0,0 +1,7 @@ +module Stekker + module MQTT + module Factories + LOAD_PATH = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec", "factories")).freeze + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb new file mode 100644 index 0000000..5f0eeb6 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb @@ -0,0 +1,24 @@ +module Stekker + module MQTT + class FakeClient + attr_reader :published_messages + + def initialize + @published_messages = [] + end + + def publish(topic, message = "", retain = false, qos = 0) # rubocop:disable Style/OptionalBooleanParameter + @published_messages << [topic, message, retain, qos] + end + + def has_published?(expected_topic, expected_message, expected_retain = false, expected_qos = 0) # rubocop:disable Style/OptionalBooleanParameter + @published_messages.any? do |(published_topic, published_message, retain, qos)| + published_topic == expected_topic && + JSON.parse(published_message) == JSON.parse(expected_message) && + retain == expected_retain && + qos == expected_qos + end + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb new file mode 100644 index 0000000..d6882be --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb @@ -0,0 +1,108 @@ +require "mqtt" + +module Stekker + module MQTT + class Listener + def initialize(url:, topics:, message_handler:, reconnect_delay_seconds: 5, connect_opts: {}, executor: nil) + @url = url + @connect_opts = connect_opts + @topics = topics.is_a?(String) ? [topics] : topics + @message_handler = message_handler + @reconnect_delay_seconds = reconnect_delay_seconds + @executor = executor + + @thread = nil + @status = :initialized + end + + def run + @status = :running + + listen_loop + end + + def start + return if running? + + @status = :running + + @thread = Thread.new do + listen_loop + + ActiveSupport::Notifications.instrument("thread_exited.listener.mqtt.stekker") + end + end + + def stop(timeout: 2) + thread = @thread + return unless thread + + @status = :stopping + + unless thread.join(timeout) + thread.kill + thread.join + end + + @thread = nil + @status = :stopped + + ActiveSupport::Notifications.instrument("stopped.listener.mqtt.stekker") + end + + def running? + !!@thread&.alive? + end + + private + + def listen_loop + loop do + ActiveSupport::Notifications.instrument("starting.listener.mqtt.stekker") + + begin + listen + + ActiveSupport::Notifications.instrument("graceful_shutdown.listener.mqtt.stekker") + rescue StandardError => e + ActiveSupport::Notifications.instrument("listen_errored.listener.mqtt.stekker", exception: e) + ensure + break if @status != :running + + sleep @reconnect_delay_seconds + end + end + end + + def listen + ::MQTT::Client.connect(@url, **@connect_opts) do |client| + ActiveSupport::Notifications.instrument("connected.listener.mqtt.stekker") + + client.subscribe(*@topics) + + ActiveSupport::Notifications.instrument("subscribed.listener.mqtt.stekker", topics: @topics) + + client.get do |topic, message| + ActiveSupport::Notifications.instrument("message_received.listener.mqtt.stekker", topic:, message:) + + wrap_execution do + @message_handler.handle_message(message) + rescue StandardError => e + ActiveSupport::Notifications.instrument("message_handling_error.listener.mqtt.stekker", exception: e) + end + rescue StandardError => e + ActiveSupport::Notifications.instrument("get_errored.listener.mqtt.stekker", exception: e) + end + end + end + + def wrap_execution(&) + if @executor + @executor.wrap(&) + else + yield + end + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb new file mode 100644 index 0000000..0c5bb09 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb @@ -0,0 +1,50 @@ +require_relative "messages" + +class Stekker::MQTT::MessageFactory + MESSAGE_TYPE_TO_MESSAGE_CLASS = { + Stekker::MQTT::Messages::ConnectMessageType::Connect => Stekker::MQTT::Messages::Connect, + Stekker::MQTT::Messages::DisconnectMessageType::Disconnect => Stekker::MQTT::Messages::Disconnect, + Stekker::MQTT::Messages::ChargeStatusMessageType::ChargeStatus => Stekker::MQTT::Messages::ChargeStatus, + Stekker::MQTT::Messages::ChargeTargetMessageType::ChargeTarget => Stekker::MQTT::Messages::ChargeTarget, + Stekker::MQTT::Messages::InstructionUpdateMessageType::InstructionUpdate => Stekker::MQTT::Messages::InstructionUpdate, + Stekker::MQTT::Messages::PowerForecastMessageType::PowerForecast => Stekker::MQTT::Messages::PowerForecast, + Stekker::MQTT::Messages::PowerMeasurementMessageType::PowerMeasurement => Stekker::MQTT::Messages::PowerMeasurement, + Stekker::MQTT::Messages::PowerSetpointMessageType::PowerSetpoint => Stekker::MQTT::Messages::PowerSetpoint, + Stekker::MQTT::Messages::S2ReadyNotificationMessageType::S2ReadyNotification => Stekker::MQTT::Messages::S2ReadyNotification, + }.freeze + + class BaseError < StandardError; end + class InvalidMessageFormat < BaseError; end + class MissingMessageType < BaseError; end + class UnsupportedMessageType < BaseError; end + class InvalidMessagePayload < BaseError; end + + class << self + def create_message(data) + message = data.is_a?(String) ? parse_json(data) : data + message_type = message["message_type"] + + raise(MissingMessageType, "Message type not provided") if message_type.to_s.empty? + + message_class = MESSAGE_TYPE_TO_MESSAGE_CLASS[message_type] + + raise(UnsupportedMessageType, "Message type not supported: #{message_type}") if message_class.nil? + + build_message(message_class, message) + end + + private + + def parse_json(data) + JSON.parse(data) + rescue JSON::ParserError + raise(InvalidMessageFormat, "Invalid JSON") + end + + def build_message(message_class, message) + message_class.from_dynamic!(message) + rescue KeyError => e + raise(InvalidMessagePayload, "Field missing in payload: #{e.key}") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb new file mode 100644 index 0000000..fc76287 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb @@ -0,0 +1,62 @@ +require "active_support/core_ext/class/attribute" +require "active_support/core_ext/object/blank" + +module Stekker + module MQTT + class MessageHandler + class HandlerNotFound < StandardError; end + + class_attribute :handlers, instance_writer: false, default: {}.freeze + class_attribute :raise_handling_errors, instance_writer: true, instance_reader: true, default: false + + class << self + def on(type, &) + raise ArgumentError, "Handler already registered for #{type}" if handler_registered?(type) + + register_handler(type, &) + end + + def handler_for(type) + handlers[type.to_s] + end + + private + + def handler_registered?(type) + handlers.has_key?(type.to_s) + end + + def register_handler(type, &block) + new_map = handlers.dup + new_map[type.to_s] = block + self.handlers = new_map.freeze + end + end + + def handle_message(payload) + message = Stekker::MQTT::MessageFactory.create_message(payload) + dispatch_message(message) + rescue StandardError => e + ActiveSupport::Notifications.instrument( + "message_errored.message_handler.mqtt.stekker", + exception: e, + payload: payload, + ) + + raise if raise_handling_errors + end + + private + + def dispatch_message(message, type: message.class) + raise ArgumentError, "Missing message type" if type.blank? + + handler = self.class.handler_for(type) + + raise(HandlerNotFound, "No handler registered for #{type}") unless handler + + instance_exec(message, &handler) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb new file mode 100644 index 0000000..73caa43 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb @@ -0,0 +1,757 @@ +# This code may look unusually verbose for Ruby (and it is), but +# it performs some subtle and complex validation of JSON data. +# +# To parse this JSON, add 'dry-struct' and 'dry-types' gems, then do: +# +# charge_status = ChargeStatus.from_json! "{…}" +# puts charge_status.data.sampled_at +# +# charge_target = ChargeTarget.from_json! "{…}" +# puts charge_target.data.direct_percentage +# +# common_defs = CommonDefs.from_json! "{…}" +# puts common_defs["…"] +# +# connect = Connect.from_json! "{…}" +# puts connect.data.maximum_current.even? +# +# disconnect = Disconnect.from_json! "{…}" +# puts disconnect.message_type == DisconnectMessageType::Disconnect +# +# instruction_update = InstructionUpdate.from_json! "{…}" +# puts instruction_update.data.reference +# +# power_forecast = PowerForecast.from_json! "{…}" +# puts power_forecast.data.values.first.timestamp +# +# power_measurement = PowerMeasurement.from_json! "{…}" +# puts power_measurement.data.sampled_at +# +# power_setpoint = PowerSetpoint.from_json! "{…}" +# puts power_setpoint.data.ends_at +# +# s2_ready_notification = S2ReadyNotification.from_json! "{…}" +# puts s2_ready_notification.message_type == S2ReadyNotificationMessageType::S2ReadyNotification +# +# If from_json! succeeds, the value returned matches the schema. + +require 'json' +require 'dry-types' +require 'dry-struct' + +module Stekker + module MQTT + module Messages + module Types + include Dry.Types(default: :nominal) + + Integer = Coercible::Integer + Hash = Coercible::Hash + String = Coercible::String + Double = Coercible::Float | Coercible::Integer + ChargeStatusMessageType = Coercible::String.enum("charge_status") + ChargeTargetMessageType = Coercible::String.enum("charge_target") + ConnectMessageType = Coercible::String.enum("connect") + DisconnectMessageType = Coercible::String.enum("disconnect") + Status = Coercible::String.enum("ABORTED", "ACCEPTED", "NEW", "REJECTED", "REVOKED", "STARTED", "SUCCEEDED") + InstructionUpdateMessageType = Coercible::String.enum("instruction_update") + Unit = Coercible::String.enum("W") + PowerForecastMessageType = Coercible::String.enum("power_forecast") + PowerMeasurementMessageType = Coercible::String.enum("power_measurement") + PowerSetpointMessageType = Coercible::String.enum("power_setpoint") + S2ReadyNotificationMessageType = Coercible::String.enum("s2_ready_notification") + end + + # Container for charge status. + class ChargeStatusData < Dry::Struct + + # Timestamp when the measurement was taken. + attribute :sampled_at, Types::String + + # Charge status as a percentage. + attribute :value, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + sampled_at: d.fetch("sampled_at"), + value: d.fetch("value"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "sampled_at" => sampled_at, + "value" => value, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module ChargeStatusMessageType + ChargeStatus = "charge_status" + end + + class ChargeStatus < Dry::Struct + + # Container for charge status. + attribute :data, ChargeStatusData + + # Fixed message type identifier. + attribute :message_type, Types::ChargeStatusMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: ChargeStatusData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Container for charge target. + class ChargeTargetData < Dry::Struct + + # The charge level to be reached as soon as possible. + attribute :direct_percentage, Types::Double + + # Timestamp when the charge target ends. + attribute :ends_at, Types::String + + # The charge level to be reached before ends_at. + attribute :smart_percentage, Types::Double + + # Timestamp when the charge target starts. + attribute :starts_at, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + direct_percentage: d.fetch("direct_percentage"), + ends_at: d.fetch("ends_at"), + smart_percentage: d.fetch("smart_percentage"), + starts_at: d.fetch("starts_at"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "direct_percentage" => direct_percentage, + "ends_at" => ends_at, + "smart_percentage" => smart_percentage, + "starts_at" => starts_at, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module ChargeTargetMessageType + ChargeTarget = "charge_target" + end + + class ChargeTarget < Dry::Struct + + # Container for charge target. + attribute :data, ChargeTargetData + + # Fixed message type identifier. + attribute :message_type, Types::ChargeTargetMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: ChargeTargetData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Container object with connection details. + class ConnectData < Dry::Struct + + # Maximum available current per phase in amperes. + attribute :maximum_current, Types::Integer + + # Minimum available current per phase in amperes. + attribute :minimum_current, Types::Integer + + # Human-readable name of the connector or resource. + attribute :data_name, Types::String + + # Number of electrical phases available. Must be 1 or 3. + attribute :number_of_phases, Types::Integer + + # Nominal voltage of the connection in volts. + attribute :voltage, Types::Integer + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + maximum_current: d.fetch("maximum_current"), + minimum_current: d.fetch("minimum_current"), + data_name: d.fetch("name"), + number_of_phases: d.fetch("number_of_phases"), + voltage: d.fetch("voltage"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "maximum_current" => maximum_current, + "minimum_current" => minimum_current, + "name" => data_name, + "number_of_phases" => number_of_phases, + "voltage" => voltage, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module ConnectMessageType + Connect = "connect" + end + + class Connect < Dry::Struct + + # Container object with connection details. + attribute :data, ConnectData + + # Fixed message type identifier. + attribute :message_type, Types::ConnectMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: ConnectData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module DisconnectMessageType + Disconnect = "disconnect" + end + + class Disconnect < Dry::Struct + + # Fixed message type identifier. + attribute :message_type, Types::DisconnectMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Instruction status. Must be one of: REJECTED, STARTED, SUCCEEDED, ABORTED. + module Status + Aborted = "ABORTED" + Accepted = "ACCEPTED" + New = "NEW" + Rejected = "REJECTED" + Revoked = "REVOKED" + Started = "STARTED" + Succeeded = "SUCCEEDED" + end + + # Instruction update details. + class InstructionUpdateData < Dry::Struct + + # Reference string identifying the instruction to update. + attribute :reference, Types::String + + # Instruction status. Must be one of: REJECTED, STARTED, SUCCEEDED, ABORTED. + attribute :status, Types::Status + + # Timestamp when the instruction status was last updated. + attribute :updated_at, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + reference: d.fetch("reference"), + status: d.fetch("status"), + updated_at: d.fetch("updated_at"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "reference" => reference, + "status" => status, + "updated_at" => updated_at, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module InstructionUpdateMessageType + InstructionUpdate = "instruction_update" + end + + class InstructionUpdate < Dry::Struct + + # Instruction update details. + attribute :data, InstructionUpdateData + + # Fixed message type identifier. + attribute :message_type, Types::InstructionUpdateMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: InstructionUpdateData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Unit of measurement. Always 'W'. + module Unit + W = "W" + end + + class Value < Dry::Struct + attribute :timestamp, Types::String + + # Forecasted power value in watts. + attribute :value, Types::Integer + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + timestamp: d.fetch("timestamp"), + value: d.fetch("value"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "timestamp" => timestamp, + "value" => value, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Container for forecast data. + class PowerForecastData < Dry::Struct + + # Start time of the forecast (ISO 8601). + attribute :start_at, Types::String + + attribute :unit, Types::Unit + + # List of forecasted power values over time. + attribute :values, Types.Array(Value) + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + start_at: d.fetch("start_at"), + unit: d.fetch("unit"), + values: d.fetch("values").map { |x| Value.from_dynamic!(x) }, + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "start_at" => start_at, + "unit" => unit, + "values" => values.map { |x| x.to_dynamic }, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module PowerForecastMessageType + PowerForecast = "power_forecast" + end + + class PowerForecast < Dry::Struct + + # Container for forecast data. + attribute :data, PowerForecastData + + # Fixed message type identifier. + attribute :message_type, Types::PowerForecastMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: PowerForecastData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Container for measurement data. + class PowerMeasurementData < Dry::Struct + + # Timestamp when the measurement was taken. + attribute :sampled_at, Types::String + + attribute :unit, Types::Unit + + # Power value in watts. + attribute :value, Types::Integer + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + sampled_at: d.fetch("sampled_at"), + unit: d.fetch("unit"), + value: d.fetch("value"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "sampled_at" => sampled_at, + "unit" => unit, + "value" => value, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module PowerMeasurementMessageType + PowerMeasurement = "power_measurement" + end + + class PowerMeasurement < Dry::Struct + + # Container for measurement data. + attribute :data, PowerMeasurementData + + # Fixed message type identifier. + attribute :message_type, Types::PowerMeasurementMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: PowerMeasurementData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Container object with power setpoint details. + class PowerSetpointData < Dry::Struct + + # End time of the power setpoint in ISO 8601 format. + attribute :ends_at, Types::String.optional + + # Unique reference string for this power setpoint instruction. + attribute :reference, Types::String + + # Start time of the power setpoint in ISO 8601 format. + attribute :starts_at, Types::String + + attribute :unit, Types::Unit + + # Maximum allowed power in watts. + attribute :value, Types::Integer + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + ends_at: d["ends_at"], + reference: d.fetch("reference"), + starts_at: d.fetch("starts_at"), + unit: d.fetch("unit"), + value: d.fetch("value"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "ends_at" => ends_at, + "reference" => reference, + "starts_at" => starts_at, + "unit" => unit, + "value" => value, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module PowerSetpointMessageType + PowerSetpoint = "power_setpoint" + end + + class PowerSetpoint < Dry::Struct + + # Container object with power setpoint details. + attribute :data, PowerSetpointData + + # Fixed message type identifier. + attribute :message_type, Types::PowerSetpointMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + data: PowerSetpointData.from_dynamic!(d.fetch("data")), + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "data" => data.to_dynamic, + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module S2ReadyNotificationMessageType + S2ReadyNotification = "s2_ready_notification" + end + + class S2ReadyNotification < Dry::Struct + + # Fixed message type identifier. + attribute :message_type, Types::S2ReadyNotificationMessageType + + attribute :resource_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + message_type: d.fetch("message_type"), + resource_id: d.fetch("resource_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "message_type" => message_type, + "resource_id" => resource_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module Stekker + module MQTT + module Messages + class CommonDefs + def self.from_json!(json) + Types::Hash[JSON.parse(json, quirks_mode: true)].map { |k, v| [k, Types::Any[v]] }.to_h + end + end + end + end + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb new file mode 100644 index 0000000..e8c113a --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb @@ -0,0 +1,35 @@ +require "mqtt" + +module Stekker + module MQTT + class Publisher + def initialize(url:, logger:) + @url = url + @logger = logger + end + + def publish(topic:, message:, retain: false, qos: 0) + with_connection { |connection| connection.publish(topic:, message:, retain:, qos:) } + end + + def with_connection + ::MQTT::Client.connect(@url) do |client| + yield Connection.new(client, @logger) + end + end + + class Connection + def initialize(client, logger) + @client = client + @logger = logger + end + + def publish(topic:, message:, retain: false, qos: 0) + json_message = message.is_a?(String) ? message : message.to_json + @client.publish(topic, json_message, retain, qos) + @logger&.log_outbound(topic:, message: json_message) + end + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb new file mode 100644 index 0000000..447c18f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb @@ -0,0 +1,94 @@ +require "json" + +RSpec::Matchers.define :have_published_mqtt_message do |topic:, message:, retain: false, qos: 0| # rubocop:disable Metrics/BlockLength + match do |client| + build_expected(topic, message, retain, qos) + collect_actual(client) + @actual_messages.any? { |m| m == @expected } + end + + match_when_negated do |client| + build_expected(topic, message, retain, qos) + collect_actual(client) + @actual_messages.none? { |m| m == @expected } + end + + failure_message do |_client| + parts = [] + parts << "Expected MQTT publish:\n#{pretty(@expected)}" + parts << if @actual_messages.empty? + "But no messages were published." + else + "But the client published:\n #{@actual_messages.map { |m| pretty(m) }.join("\n")}" + end + parts.join("\n") + end + + failure_message_when_negated do |_client| + "Did not expect MQTT publish, but found:\n#{pretty(@expected)}" + end + + description do + "publish to #{topic.inspect} with retain=#{retain} and qos=#{qos}" + end + + diffable + + attr_reader :expected + + def actual + (@actual_messages&.find { |m| m[:topic] == @expected[:topic] } || + @actual_messages&.first) || {} + end + + # Helpers + def build_expected(topic, message, retain, qos) + @expected = { + topic: topic, + payload: normalize_payload(message), + retain: retain, + qos: qos, + } + end + + def collect_actual(client) + raw = client.published_messages + @actual_messages = raw.map do |(t, m, r, q)| + { topic: t, payload: normalize_payload(m), retain: r, qos: q } + end + end + + def normalize_payload(obj) + case obj + when String + begin + deep_stringify_keys(JSON.parse(obj)) + rescue JSON::ParserError + obj + end + when Hash, Array + deep_stringify_keys(obj) + else + obj + end + end + + def deep_stringify_keys(value) + case value + when Hash + value.each_with_object({}) do |(k, v), acc| + acc[k.to_s] = deep_stringify_keys(v) + end + when Array + value.map { |v| deep_stringify_keys(v) } + else + value + end + end + + def pretty(obj) + JSON.pretty_generate(obj) + rescue StandardError + obj.inspect + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb new file mode 100644 index 0000000..2da4119 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb @@ -0,0 +1,5 @@ +module Stekker + module MQTT + VERSION = "0.1.0".freeze + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb new file mode 100644 index 0000000..71ff873 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb @@ -0,0 +1,17 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_charge_status, class: "Stekker::MQTT::Messages::ChargeStatus" do + initialize_with { Stekker::MQTT::Messages::ChargeStatus.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::ChargeStatusMessageType::ChargeStatus } + + data do + { + sampled_at: Time.current.iso8601, + value: 50, + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb new file mode 100644 index 0000000..61575c7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb @@ -0,0 +1,19 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_charge_target, class: "Stekker::MQTT::Messages::ChargeTarget" do + initialize_with { Stekker::MQTT::Messages::ChargeTarget.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::ChargeTargetMessageType::ChargeTarget } + + data do + { + starts_at: Time.current.iso8601, + ends_at: 8.hours.from_now.iso8601, + direct_percentage: 25.0, + smart_percentage: 100.0, + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb new file mode 100644 index 0000000..bdc9c28 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb @@ -0,0 +1,20 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_connect, class: "Stekker::MQTT::Messages::Connect" do + initialize_with { Stekker::MQTT::Messages::Connect.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::ConnectMessageType::Connect } + + data do + { + name: "My charger - #1", + voltage: 230, + number_of_phases: 3, + minimum_current: 0, + maximum_current: 16, + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb new file mode 100644 index 0000000..522b45d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb @@ -0,0 +1,10 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_disconnect, class: "Stekker::MQTT::Messages::Disconnect" do + initialize_with { Stekker::MQTT::Messages::Disconnect.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::DisconnectMessageType::Disconnect } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb new file mode 100644 index 0000000..3daf893 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb @@ -0,0 +1,18 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_instruction_update, class: "Stekker::MQTT::Messages::InstructionUpdate" do + initialize_with { Stekker::MQTT::Messages::InstructionUpdate.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::InstructionUpdateMessageType::InstructionUpdate } + + data do + { + reference: SecureRandom.uuid, + updated_at: Time.current.iso8601, + status: Stekker::MQTT::Messages::Status::Succeeded, + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb new file mode 100644 index 0000000..6911430 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb @@ -0,0 +1,22 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_power_forecast, class: "Stekker::MQTT::Messages::PowerForecast" do + initialize_with { Stekker::MQTT::Messages::PowerForecast.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::PowerForecastMessageType::PowerForecast } + + data do + { + start_at: Time.current.iso8601, + unit: "W", + values: [ + { timestamp: 5.minutes.from_now.iso8601, value: 11_040 }, + { timestamp: 15.minutes.from_now.iso8601, value: 6_720 }, + { timestamp: 35.minutes.from_now.iso8601, value: 0 }, + ], + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb new file mode 100644 index 0000000..15cb83e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb @@ -0,0 +1,18 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_power_measurement, class: "Stekker::MQTT::Messages::PowerMeasurement" do + initialize_with { Stekker::MQTT::Messages::PowerMeasurement.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::PowerMeasurementMessageType::PowerMeasurement } + + data do + { + sampled_at: Time.current.iso8601, + value: 1500, + unit: "W", + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb new file mode 100644 index 0000000..df9cc84 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb @@ -0,0 +1,19 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_power_setpoint, class: "Stekker::MQTT::Messages::PowerSetpoint" do + initialize_with { Stekker::MQTT::Messages::PowerSetpoint.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::PowerSetpointMessageType::PowerSetpoint } + + data do + { + reference: SecureRandom.uuid, + starts_at: Time.current.iso8601, + value: 1500, + unit: "W", + } + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb new file mode 100644 index 0000000..656a321 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb @@ -0,0 +1,10 @@ +require "factory_bot" + +FactoryBot.define do + factory :stekker_mqtt_s2_ready_notification, class: "Stekker::MQTT::Messages::S2ReadyNotification" do + initialize_with { Stekker::MQTT::Messages::S2ReadyNotification.from_dynamic!(attributes.with_indifferent_access) } + + resource_id { SecureRandom.uuid } + message_type { Stekker::MQTT::Messages::S2ReadyNotificationMessageType::S2ReadyNotification } + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb new file mode 100644 index 0000000..9433bf2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb @@ -0,0 +1,26 @@ +require_relative "../lib/stekker/mqtt" + +require "support/factory_bot" +require "active_support/core_ext/hash/indifferent_access" + +RSpec.configure do |config| + Kernel.srand config.seed + config.disable_monkey_patching! + config.example_status_persistence_file_path = "spec/examples.txt" + config.expose_dsl_globally = true + config.filter_run_when_matching :focus + config.order = :random + config.profile_examples = 10 + config.shared_context_metadata_behavior = :apply_to_host_groups + config.warnings = true + + config.default_formatter = "doc" if config.files_to_run.one? + + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb new file mode 100644 index 0000000..5cf0777 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb @@ -0,0 +1,126 @@ +describe Stekker::MQTT::Listener do + describe "#start" do + it "subscribes to the specified topics and processes messages" do + url = "mqtt://localhost" + topics = %w[test/topic1 test/topic2] + message_handler = instance_double(Stekker::MQTT::MessageHandler) + allow(message_handler).to receive(:handle_message) + listener = described_class.new(url:, topics:, message_handler:) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:subscribe).with(*topics) + allow(client).to receive(:get) + .and_yield("test/topic1", "Test message 1") + .and_yield("test/topic2", "Test message 2") + + listener.start + + sleep 0.01 + + expect(message_handler).to have_received(:handle_message).with("Test message 1") + expect(message_handler).to have_received(:handle_message).with("Test message 2") + end + + it "instruments errors when message handling fails" do + url = "mqtt://localhost" + topics = %w[test/topic] + message_handler = instance_double(Stekker::MQTT::MessageHandler) + allow(message_handler).to receive(:handle_message) + listener = described_class.new(url:, topics:, message_handler:) + + client_double = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client_double) + allow(client_double).to receive(:subscribe).with(*topics) + allow(client_double).to receive(:get).and_yield("test/topic", "Faulty message") + allow(ActiveSupport::Notifications).to receive(:instrument) + + error = StandardError.new("Handler error") + allow(message_handler).to receive(:handle_message).with("Faulty message").and_raise(error) + + listener.start + + sleep 0.01 + + expect(ActiveSupport::Notifications).to have_received(:instrument) + .with("message_handling_error.listener.mqtt.stekker", exception: error) + end + end + + describe "#run" do + it "listens for messages without spawning a thread" do + url = "mqtt://localhost" + topics = %w[test/topic] + message_handler = instance_double(Stekker::MQTT::MessageHandler) + listener = described_class.new(url:, topics:, message_handler:, reconnect_delay_seconds: 0) + + allow(message_handler).to receive(:handle_message) do + listener.instance_variable_set(:@status, :stopping) + end + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:subscribe).with(*topics) + allow(client).to receive(:get).and_yield("test/topic", "Test message") + + listener.run + + expect(message_handler).to have_received(:handle_message).with("Test message") + expect(listener).not_to be_running + end + end + + describe "executor" do + it "wraps message handling in the executor when provided" do + url = "mqtt://localhost" + topics = %w[test/topic] + message_handler = instance_double(Stekker::MQTT::MessageHandler) + + executor = class_double(ActiveSupport::Executor) + allow(executor).to receive(:wrap).and_yield + + listener = described_class.new(url:, topics:, message_handler:, executor:, reconnect_delay_seconds: 0) + + allow(message_handler).to receive(:handle_message) do + listener.instance_variable_set(:@status, :stopping) + end + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:subscribe).with(*topics) + allow(client).to receive(:get).and_yield("test/topic", "Test message") + + listener.run + + expect(executor).to have_received(:wrap).once + expect(message_handler).to have_received(:handle_message).with("Test message") + end + end + + describe "#stop" do + it "stops the listener thread" do + url = "mqtt://localhost" + topics = %w[test/topic] + message_handler = instance_double(Stekker::MQTT::MessageHandler) + allow(message_handler).to receive(:handle_message) + listener = described_class.new(url:, topics:, message_handler:) + + client_double = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client_double) + allow(client_double).to receive(:subscribe).with(*topics) + allow(client_double).to receive(:get).and_yield("test/topic", "Test message") + + listener.start + + sleep 0.01 + + expect(listener).to be_running + + listener.stop(timeout: 0.01) + + sleep 0.01 + + expect(listener).not_to be_running + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb new file mode 100644 index 0000000..850cb11 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb @@ -0,0 +1,90 @@ +describe Stekker::MQTT::MessageFactory do + describe "::create_message" do + it "creates a message object from valid JSON data" do + json_data = <<~JSON + { + "message_type": "power_measurement", + "resource_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "data": { + "sampled_at": "2024-10-01T12:00:00Z", + "unit": "W", + "value": 1500 + } + } + JSON + + message = described_class.create_message(json_data) + + expect(message).to be_a(Stekker::MQTT::Messages::PowerMeasurement) + + expect(message).to have_attributes( + message_type: "power_measurement", + resource_id: "ce7b430c-25be-41ec-b97c-ac0258aff498", + data: have_attributes( + sampled_at: "2024-10-01T12:00:00Z", + unit: "W", + value: 1500, + ), + ) + end + + it "raises InvalidMessageFormatError for invalid JSON data" do + invalid_json_data = "{ invalid json }" + + expect { described_class.create_message(invalid_json_data) } + .to raise_error(Stekker::MQTT::MessageFactory::InvalidMessageFormat, "Invalid JSON") + end + + it "raises MissingMessageTypeError when message_type is missing" do + json_data = <<~JSON + { + "resource_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "data": { + "sampled_at": "2024-10-01T12:00:00Z", + "unit": "W", + "value": 1500.5 + } + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error(Stekker::MQTT::MessageFactory::MissingMessageType, "Message type not provided") + end + + it "raises UnsupportedMessageTypeError for unsupported message_type" do + json_data = <<~JSON + { + "message_type": "UnknownType", + "resource_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "data": { + "sampled_at": "2024-10-01T12:00:00Z", + "unit": "W", + "value": 1500.5 + } + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error( + Stekker::MQTT::MessageFactory::UnsupportedMessageType, + /Message type not supported: UnknownType/, + ) + end + + it "raises InvalidMessagePayloadError for missing required fields" do + json_data = <<~JSON + { + "message_type": "power_measurement", + "resource_id": "ce7b430c-25be-41ec-b97c-ac0258aff498", + "data": { + "sampled_at": "2024-10-01T12:00:00Z", + "value": 1500.5 + } + } + JSON + + expect { described_class.create_message(json_data) } + .to raise_error(Stekker::MQTT::MessageFactory::InvalidMessagePayload, /Field missing in payload: unit/) + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb new file mode 100644 index 0000000..21d1c24 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb @@ -0,0 +1,66 @@ +describe Stekker::MQTT::MessageHandler do + describe "::on" do + it "registers a handler for the specified message class" do + handler_block = proc {} + + message_handler_class = Class.new(Stekker::MQTT::MessageHandler) do + on Stekker::MQTT::Messages::Connect, &handler_block + end + + expect(message_handler_class.handlers[Stekker::MQTT::Messages::Connect.name]).to eq(handler_block) + end + + it "raises an error when defining a second handler for the same message" do + handler_block = proc {} + + message_handler_class = Class.new(Stekker::MQTT::MessageHandler) do + on Stekker::MQTT::Messages::Connect, &handler_block + end + + expect { message_handler_class.on(Stekker::MQTT::Messages::Connect, &handler_block) } + .to raise_error(/Handler already registered for Stekker::MQTT::Messages::Connect/) + end + end + + describe "#handle_message" do + it "invokes the registered handler for the message" do + handler_invoked = false + handler_block = proc { |message| handler_invoked = message.instance_of?(Stekker::MQTT::Messages::Connect) } + + message_handler_class = Class.new(Stekker::MQTT::MessageHandler) do + on Stekker::MQTT::Messages::Connect, &handler_block + end + + handler = message_handler_class.new + message = build(:stekker_mqtt_connect) + + handler.handle_message(message.to_json) + expect(handler_invoked).to be true + end + + it "instruments an error event when message handling fails" do + handler_block = proc { |_message| raise StandardError, "Handler error" } + + message_handler_class = Class.new(Stekker::MQTT::MessageHandler) do + on Stekker::MQTT::Messages::Connect, &handler_block + end + + message = build(:stekker_mqtt_connect) + payload = message.to_json + + callback = ->(*args) do + event = ActiveSupport::Notifications::Event.new(*args) + expect(event.name).to eq("message_errored.message_handler.mqtt.stekker") + expect(event.payload[:payload]).to eq(payload) + expect(event.payload[:exception]).to be_a(StandardError) + expect(event.payload[:exception].message).to eq("Handler error") + end + + handler = message_handler_class.new + + ActiveSupport::Notifications.subscribed(callback, "message_errored.message_handler.mqtt.stekker") do + handler.handle_message(payload) + end + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb new file mode 100644 index 0000000..b388bd6 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb @@ -0,0 +1,129 @@ +class FakeLogger + def log_outbound(topic:, message:) + end +end + +describe Stekker::MQTT::Publisher do + describe "#publish" do + it "publishes the message to the MQTT broker with default retain and qos" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger:) + publisher.publish(topic: "test/topic", message: "Test message") + + expect(client).to have_received(:publish).with("test/topic", "Test message", false, 0) + end + + it "publishes with custom qos" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger:) + publisher.publish(topic: "test/topic", message: "Test message", qos: 1) + + expect(client).to have_received(:publish).with("test/topic", "Test message", false, 1) + end + + it "publishes with retain flag" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger:) + publisher.publish(topic: "test/topic", message: "Test message", retain: true) + + expect(client).to have_received(:publish).with("test/topic", "Test message", true, 0) + end + + it "logs the outbound message" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger:) + publisher.publish(topic: "test/topic", message: "Test message") + + expect(logger).to have_received(:log_outbound).with( + topic: "test/topic", + message: "Test message", + ) + end + + it "converts message to JSON when it responds to to_json" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + message = instance_double(Stekker::MQTT::Messages::PowerMeasurement, to_json: '{"key":"value"}') + + publisher = described_class.new(url:, logger:) + publisher.publish(topic: "test/topic", message:) + + expect(client).to have_received(:publish).with("test/topic", '{"key":"value"}', false, 0) + expect(logger).to have_received(:log_outbound).with( + topic: "test/topic", + message: '{"key":"value"}', + ) + end + + it "works without a logger" do + url = "mqtt://localhost" + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger: nil) + publisher.publish(topic: "test/topic", message: "Test message") + + expect(client).to have_received(:publish).with("test/topic", "Test message", false, 0) + end + end + + describe "#with_connection" do + it "yields a connection that can publish multiple messages" do + url = "mqtt://localhost" + logger = instance_double(FakeLogger) + allow(logger).to receive(:log_outbound) + + client = instance_double(MQTT::Client) + allow(MQTT::Client).to receive(:connect).with(url).and_yield(client) + allow(client).to receive(:publish) + + publisher = described_class.new(url:, logger:) + publisher.with_connection do |connection| + connection.publish(topic: "topic/1", message: "Message 1") + connection.publish(topic: "topic/2", message: "Message 2", qos: 1) + end + + expect(MQTT::Client).to have_received(:connect).once + expect(client).to have_received(:publish).with("topic/1", "Message 1", false, 0) + expect(client).to have_received(:publish).with("topic/2", "Message 2", false, 1) + expect(logger).to have_received(:log_outbound).with(topic: "topic/1", message: "Message 1") + expect(logger).to have_received(:log_outbound).with(topic: "topic/2", message: "Message 2") + end + end +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb new file mode 100644 index 0000000..bd54bb1 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb @@ -0,0 +1,7 @@ +require "factory_bot" + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + + FactoryBot.find_definitions +end diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec new file mode 100644 index 0000000..6a6cd9f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec @@ -0,0 +1,30 @@ +require_relative "lib/stekker/mqtt/version" + +Gem::Specification.new do |spec| + spec.name = "stekker-mqtt" + spec.version = Stekker::MQTT::VERSION + spec.authors = ["Stekker.app B.V."] + + spec.summary = "Stekker MQTT integration" + spec.description = "Stekker MQTT integration" + spec.homepage = "https://stekker.com" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.4.1" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["rubygems_mfa_required"] = "true" + + spec.files = Dir.chdir(__dir__) do + Dir.glob("lib/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) } + + Dir.glob("{README*,LICENSE*,CHANGELOG*}").select { |f| File.file?(f) } + end + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "activesupport", ">= 6.0" + spec.add_dependency "dry-struct" + spec.add_dependency "dry-types" + spec.add_dependency "mqtt" +end From 9bf3ff169041503dfad263bb04c0a4bfeee0ec6c Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 11 Jun 2026 15:23:47 +0200 Subject: [PATCH 02/10] Publish charger events from a demo MQTT platform The bridge needs resource-side traffic to translate, and the real charging platform behind the MQTT contract is not open source. This stand-in bundles the broker and a small panel that publishes the example payloads and shows the power setpoints coming back, so the forward and reverse paths are drivable without any external system. Co-authored-by: Bob Forma Co-authored-by: Erik de Bruijn Co-authored-by: Martijn Versluis --- resource-manager-mqtt-bridge-ruby/README.md | 8 + .../mqtt/Dockerfile | 7 + .../mqtt/docker-compose.yml | 20 ++ .../mqtt/examples/charge_status.json | 8 + .../mqtt/examples/charge_target.json | 10 + .../mqtt/examples/connect.json | 11 ++ .../mqtt/examples/disconnect.json | 4 + .../mqtt/examples/power_forecast.json | 22 +++ .../mqtt/examples/power_measurement.json | 9 + .../mqtt/mosquitto.conf | 2 + .../mqtt/package.json | 14 ++ .../mqtt/public/index.html | 158 ++++++++++++++++ .../mqtt/public/tokens.css | 174 ++++++++++++++++++ .../mqtt/server.js | 110 +++++++++++ 14 files changed, 557 insertions(+) create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/package.json create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/public/index.html create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css create mode 100644 resource-manager-mqtt-bridge-ruby/mqtt/server.js diff --git a/resource-manager-mqtt-bridge-ruby/README.md b/resource-manager-mqtt-bridge-ruby/README.md index 02f4076..1384329 100644 --- a/resource-manager-mqtt-bridge-ruby/README.md +++ b/resource-manager-mqtt-bridge-ruby/README.md @@ -123,6 +123,14 @@ docker compose logs -f app Each `mosquitto_pub` also appears in the dashboard's "RM messages" feed at http://localhost:3000. +## Drive it from a demo charging platform + +Instead of `mosquitto_pub`, the [`mqtt/`](mqtt/) directory contains the broker and a small panel that stands +in for the real charging platform (which is not open source): one button per bundled example event, and a +feed showing the `power_setpoint` and `instruction_update` messages that come back. From `mqtt/`, run +`docker compose up --build`; the panel listens on http://localhost:4000 and the broker on port 1883. Point +the RM's `MQTT_URL` at that broker to drive the bridge from the panel. + ## MQTT topics and S2 mapping Every bundled example is a real resource-side message the bridge already supports. The S2 column is the diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile b/resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile new file mode 100644 index 0000000..69d6d44 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev +COPY . . +EXPOSE 4000 +CMD ["node", "server.js"] diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml b/resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml new file mode 100644 index 0000000..e5a82b7 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml @@ -0,0 +1,20 @@ +name: s2-demo-mqtt + +services: + mosquitto: + image: eclipse-mosquitto:2 + ports: + - "1883:1883" + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + + mqtt-panel: + build: . + depends_on: + - mosquitto + environment: + PORT: "4000" + MQTT_URL: "mqtt://mosquitto:1883" + RESOURCE_ID: "demo-resource-001" + ports: + - "4000:4000" diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json new file mode 100644 index 0000000..668a0d2 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json @@ -0,0 +1,8 @@ +{ + "message_type": "charge_status", + "resource_id": "demo-resource-001", + "data": { + "sampled_at": "2026-06-04T10:05:00Z", + "value": 42.5 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json new file mode 100644 index 0000000..4a1ff72 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json @@ -0,0 +1,10 @@ +{ + "message_type": "charge_target", + "resource_id": "demo-resource-001", + "data": { + "starts_at": "2026-06-04T10:00:00Z", + "ends_at": "2026-06-04T18:00:00Z", + "direct_percentage": 25.0, + "smart_percentage": 85.0 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json new file mode 100644 index 0000000..1d2fde9 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json @@ -0,0 +1,11 @@ +{ + "message_type": "connect", + "resource_id": "demo-resource-001", + "data": { + "name": "Demo EV charger", + "voltage": 230, + "number_of_phases": 3, + "minimum_current": 0, + "maximum_current": 16 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json new file mode 100644 index 0000000..993af8d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json @@ -0,0 +1,4 @@ +{ + "message_type": "disconnect", + "resource_id": "demo-resource-001" +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json new file mode 100644 index 0000000..c7f35d0 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json @@ -0,0 +1,22 @@ +{ + "message_type": "power_forecast", + "resource_id": "demo-resource-001", + "data": { + "start_at": "2026-06-04T10:00:00Z", + "unit": "W", + "values": [ + { + "timestamp": "2026-06-04T10:00:00Z", + "value": 3700 + }, + { + "timestamp": "2026-06-04T10:15:00Z", + "value": 7400 + }, + { + "timestamp": "2026-06-04T10:45:00Z", + "value": 0 + } + ] + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json b/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json new file mode 100644 index 0000000..ea28589 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json @@ -0,0 +1,9 @@ +{ + "message_type": "power_measurement", + "resource_id": "demo-resource-001", + "data": { + "sampled_at": "2026-06-04T10:00:00Z", + "unit": "W", + "value": 3700 + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf b/resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf new file mode 100644 index 0000000..c8348ac --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 +allow_anonymous true diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/package.json b/resource-manager-mqtt-bridge-ruby/mqtt/package.json new file mode 100644 index 0000000..f00c139 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/package.json @@ -0,0 +1,14 @@ +{ + "name": "mqtt-platform-demo", + "version": "0.0.0", + "private": true, + "description": "Demo stand-in for the Stekker smart-charging platform: publishes resource-side MQTT events and shows the RM's reverse-path replies.", + "type": "module", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.21.2", + "mqtt": "^5.10.1" + } +} diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/public/index.html b/resource-manager-mqtt-bridge-ruby/mqtt/public/index.html new file mode 100644 index 0000000..1f9f127 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/public/index.html @@ -0,0 +1,158 @@ + + + + + + MQTT platform (demo) + + + + +
+
+
MQTT Platformdemo stand-in
+
Simulates the charger over MQTT: publishes its events and shows the traffic with the RM in both directions.
+
+ +
+
+
+ + +
No messages yet.
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css b/resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css new file mode 100644 index 0000000..4fc9565 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css @@ -0,0 +1,174 @@ +:root { + --bg: #eef1f5; + --panel: #ffffff; + --panel-2: #f3f6f9; + --border: #dce2e9; + --border-strong: #c5cdd7; + --text: #1b2733; + --muted: #51606f; + --faint: #6b7685; + + --brand: #0d8f61; + --brand-ink: #0a7a52; + --brand-deep: #08643f; + --brand-soft: #e6f6ef; + + --report: #2563b0; + --report-ink: #1f4f8f; + --report-soft: #e8effb; + + --command: #a85c0a; + --command-ink: #8a4c08; + --command-soft: #fbf0df; + + --setup: #6b7685; + + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --radius: 10px; + --radius-sm: 7px; + --gap: 14px; + --header-min: 84px; + --primary-min: 76px; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 14px; + -webkit-font-smoothing: antialiased; +} + +.panel { + display: flex; + flex-direction: column; + height: 100vh; + padding: var(--gap); + gap: var(--gap); + background: var(--bg); +} + +.panel-header { + display: flex; + flex-direction: column; + gap: 4px; + min-height: var(--header-min); + padding-bottom: var(--gap); + border-bottom: 1px solid var(--border); +} + +.primary { + display: flex; + flex-direction: column; + gap: 10px; + min-height: var(--primary-min); +} + +.panel-title { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.1px; + color: var(--text); +} + +.panel-role { + font-size: 12.5px; + line-height: 1.45; + color: var(--muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.panel-tag { + margin-left: 9px; + font-size: 12px; + font-weight: 550; + letter-spacing: 0; + color: var(--faint); +} + +.panel-tag.is-reference { + color: var(--text); + font-weight: 650; +} + +.section-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.7px; + font-weight: 650; + color: var(--faint); + margin: 2px 0; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.btn { + font-family: var(--sans); + font-size: 13px; + font-weight: 600; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: 8px 12px; + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; +} + +.btn:hover { border-color: var(--brand); background: var(--brand-soft); } +.btn:active { background: var(--panel-2); } +.btn:disabled { opacity: 0.55; cursor: default; } +.btn-primary { + border-color: var(--brand-ink); + background: var(--brand-ink); + color: #ffffff; +} +.btn-primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); } + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px; +} + +.feed { + flex: 1; + overflow-y: auto; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + font-size: 13px; + line-height: 1.5; +} + +.empty { color: var(--faint); font-style: italic; } + +.tstamp { + font-family: var(--mono); + font-size: 10.5px; + color: var(--faint); + white-space: nowrap; +} + +.statusline { + display: flex; + flex-wrap: wrap; + gap: 3px 20px; + font-size: 12.5px; + color: var(--muted); +} +.statusline .live { color: var(--brand-ink); font-weight: 600; } diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/server.js b/resource-manager-mqtt-bridge-ruby/mqtt/server.js new file mode 100644 index 0000000..c5db202 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/mqtt/server.js @@ -0,0 +1,110 @@ +import express from "express"; +import mqtt from "mqtt"; +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PORT = process.env.PORT || 4000; +const BROKER_URL = process.env.MQTT_URL || "mqtt://localhost:1883"; +const RESOURCE_ID = process.env.RESOURCE_ID || "demo-resource-001"; + +const TOPIC_SUFFIX_BY_MESSAGE_TYPE = { + connect: "connects", + disconnect: "disconnects", + power_measurement: "power_measurements", + power_forecast: "power_forecasts", + charge_target: "charge_targets", + charge_status: "charge_statuses", +}; + +const REVERSE_PATH_SUFFIXES = ["power_setpoints", "instruction_updates", "s2_ready_notifications"]; +const RECEIVED_BUFFER_LIMIT = 50; + +const examplesDir = join(__dirname, "examples"); +const examplePayloadByType = {}; +for (const file of readdirSync(examplesDir).filter((f) => f.endsWith(".json"))) { + const type = file.replace(/\.json$/, ""); + examplePayloadByType[type] = JSON.parse(readFileSync(join(examplesDir, file), "utf8")); +} + +function topicFor(suffix) { + return `stekker/resources/${RESOURCE_ID}/${suffix}`; +} + +const feed = []; +let chargerConnected = false; +function remember(entry) { + feed.unshift({ ...entry, at: new Date().toISOString() }); + if (feed.length > RECEIVED_BUFFER_LIMIT) feed.pop(); +} + +const client = mqtt.connect(BROKER_URL, { reconnectPeriod: 2000 }); +client.on("connect", () => { + for (const suffix of REVERSE_PATH_SUFFIXES) client.subscribe(topicFor(suffix)); + console.log(`[mqtt-panel] connected to ${BROKER_URL}, subscribed to reverse-path topics`); +}); +client.on("message", (incomingTopic, payload) => { + let body; + try { + body = JSON.parse(payload.toString()); + } catch { + body = payload.toString(); + } + remember({ dir: "in", topic: incomingTopic, message_type: body?.message_type, body }); +}); +client.on("error", (err) => console.error("[mqtt-panel] broker error:", err.message)); + +const app = express(); +app.use((_req, res, next) => { + res.setHeader("Cache-Control", "no-store"); + next(); +}); +app.use(express.static(join(__dirname, "public"), { etag: false, lastModified: false, cacheControl: false })); + +app.get("/api/examples", (_req, res) => { + res.json( + Object.keys(examplePayloadByType) + .filter((type) => TOPIC_SUFFIX_BY_MESSAGE_TYPE[type]) + .map((type) => ({ + type, + topic: topicFor(TOPIC_SUFFIX_BY_MESSAGE_TYPE[type]), + resource_id: RESOURCE_ID, + })), + ); +}); + +app.post("/api/publish/:type", (req, res) => { + const { type } = req.params; + const payload = examplePayloadByType[type]; + const suffix = TOPIC_SUFFIX_BY_MESSAGE_TYPE[type]; + if (!payload || !suffix) { + res.status(404).json({ error: `unknown example '${type}'` }); + return; + } + const targetTopic = topicFor(suffix); + client.publish(targetTopic, JSON.stringify(payload), { qos: 1 }, (err) => { + if (err) { + res.status(502).json({ error: err.message }); + return; + } + console.log(`[mqtt-panel] published ${type} -> ${targetTopic}`); + if (type === "connect") chargerConnected = true; + if (type === "disconnect") chargerConnected = false; + remember({ dir: "out", topic: targetTopic, message_type: payload.message_type, body: payload }); + res.json({ published: type, topic: targetTopic }); + }); +}); + +app.get("/api/received", (_req, res) => res.json(feed)); + +app.get("/api/state", (_req, res) => res.json({ charger_connected: chargerConnected })); + +app.post("/api/reset", (_req, res) => { + feed.length = 0; + res.json({ reset: true }); +}); + +app.get("/healthz", (_req, res) => res.json({ ok: true, broker: BROKER_URL })); + +app.listen(PORT, () => console.log(`[mqtt-panel] listening on :${PORT}, broker ${BROKER_URL}`)); From c9cb7aaab203ffeccc3a4dd0e09ccbae06b60862 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 11 Jun 2026 15:24:03 +0200 Subject: [PATCH 03/10] Close the S2 loop with a demo CEM counterpart Stand-alone, the RM has no peer to complete the S2 handshake with. This minimal WebSocket server replies with its own Handshake, selects FILL_RATE_BASED_CONTROL, and sends an FRBC.Instruction built from the FRBC.SystemDescription it received from the RM, so the closed loop runs on real protocol traffic rather than scripted state. Co-authored-by: Bob Forma Co-authored-by: Erik de Bruijn Co-authored-by: Martijn Versluis --- resource-manager-mqtt-bridge-ruby/README.md | 12 +- .../cem/Dockerfile | 14 + resource-manager-mqtt-bridge-ruby/cem/Gemfile | 6 + .../cem/Gemfile.lock | 134 ++++++++++ .../cem/docker-compose.yml | 9 + .../cem/public/index.html | 150 +++++++++++ .../cem/public/tokens.css | 174 +++++++++++++ .../cem/server.rb | 241 ++++++++++++++++++ 8 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 resource-manager-mqtt-bridge-ruby/cem/Dockerfile create mode 100644 resource-manager-mqtt-bridge-ruby/cem/Gemfile create mode 100644 resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock create mode 100644 resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml create mode 100644 resource-manager-mqtt-bridge-ruby/cem/public/index.html create mode 100644 resource-manager-mqtt-bridge-ruby/cem/public/tokens.css create mode 100644 resource-manager-mqtt-bridge-ruby/cem/server.rb diff --git a/resource-manager-mqtt-bridge-ruby/README.md b/resource-manager-mqtt-bridge-ruby/README.md index 1384329..9127311 100644 --- a/resource-manager-mqtt-bridge-ruby/README.md +++ b/resource-manager-mqtt-bridge-ruby/README.md @@ -43,7 +43,7 @@ flowchart LR client["Generic MQTT client
(mosquitto_pub)"] -->|publish| broker[("Mosquitto
broker")] broker -->|"stekker/resources/+/…"| app["Rails S2 Resource
Manager (RM)"] app --- db[("Postgres")] - app -->|"S2 over WebSocket"| cem["CEM
(placeholder when standalone)"] + app -->|"S2 over WebSocket"| cem["CEM
(demo CEM, or a
placeholder when standalone)"] browser["Browser"] -->|"reads the read-only dashboard"| app ``` @@ -131,6 +131,16 @@ feed showing the `power_setpoint` and `instruction_update` messages that come ba `docker compose up --build`; the panel listens on http://localhost:4000 and the broker on port 1883. Point the RM's `MQTT_URL` at that broker to drive the bridge from the panel. +## Close the loop with a demo CEM + +The [`cem/`](cem/) directory holds a minimal S2 CEM counterpart: a WebSocket server that completes the +handshake (replying with its own `Handshake`, role CEM), selects `FILL_RATE_BASED_CONTROL`, acknowledges +every message with `ReceptionStatus`, and sends an `FRBC.Instruction` derived from the +`FRBC.SystemDescription` it received from the RM. From `cem/`, run `docker compose up --build`; the panel +and WebSocket server listen on http://localhost:9000. Point the RM's `CEM_BASE_URL` at `ws://:9000/s2` +and the closed loop runs on real protocol traffic: an instruction from the CEM panel comes back as a +`power_setpoint` on MQTT. + ## MQTT topics and S2 mapping Every bundled example is a real resource-side message the bridge already supports. The S2 column is the diff --git a/resource-manager-mqtt-bridge-ruby/cem/Dockerfile b/resource-manager-mqtt-bridge-ruby/cem/Dockerfile new file mode 100644 index 0000000..f47985d --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/Dockerfile @@ -0,0 +1,14 @@ +FROM ruby:3.4-slim + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y git build-essential && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY Gemfile Gemfile.lock ./ +RUN bundle install + +COPY . . + +EXPOSE 9000 +CMD ["ruby", "server.rb"] diff --git a/resource-manager-mqtt-bridge-ruby/cem/Gemfile b/resource-manager-mqtt-bridge-ruby/cem/Gemfile new file mode 100644 index 0000000..5178c06 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "s2-ruby", github: "stekker/s2-ruby", ref: "18583316a64e1d27e4273fe64a73988562848a6d", require: "s2" +gem "async" +gem "async-http" +gem "async-websocket" diff --git a/resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock b/resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock new file mode 100644 index 0000000..8ba2914 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock @@ -0,0 +1,134 @@ +GIT + remote: https://github.com/stekker/s2-ruby.git + revision: 18583316a64e1d27e4273fe64a73988562848a6d + ref: 18583316a64e1d27e4273fe64a73988562848a6d + specs: + s2-ruby (0.1.0) + activesupport (>= 7.0) + async + async-websocket + dry-struct (~> 1.6) + dry-types (~> 1.7) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + async (2.39.0) + console (~> 1.29) + fiber-annotation + io-event (~> 1.11) + metrics (~> 0.12) + traces (~> 0.18) + async-http (0.95.1) + async (>= 2.10.2) + async-pool (~> 0.11) + io-endpoint (~> 0.14) + io-stream (~> 0.6) + metrics (~> 0.12) + protocol-http (~> 0.62) + protocol-http1 (~> 0.39) + protocol-http2 (~> 0.26) + protocol-url (~> 0.2) + traces (~> 0.10) + async-pool (0.11.2) + async (>= 2.0) + async-websocket (0.30.0) + async-http (~> 0.76) + protocol-http (~> 0.34) + protocol-rack (~> 0.7) + protocol-websocket (~> 0.17) + base64 (0.3.0) + bigdecimal (4.1.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + console (1.36.0) + fiber-annotation + fiber-local (~> 1.1) + json + drb (2.2.3) + dry-core (1.2.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.3.1) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-struct (1.8.1) + dry-core (~> 1.1) + dry-types (~> 1.8, >= 1.8.2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.9.1) + bigdecimal (>= 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + io-endpoint (0.17.2) + io-event (1.16.1) + io-stream (0.13.0) + json (2.19.8) + logger (1.7.0) + metrics (0.15.0) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + prism (1.9.0) + protocol-hpack (1.5.1) + protocol-http (0.62.2) + protocol-http1 (0.39.0) + protocol-http (~> 0.62) + protocol-http2 (0.26.0) + protocol-hpack (~> 1.4) + protocol-http (~> 0.62) + protocol-rack (0.22.1) + io-stream (>= 0.10) + protocol-http (~> 0.58) + rack (>= 1.0) + protocol-url (0.4.0) + protocol-websocket (0.21.1) + protocol-http (~> 0.2) + rack (3.2.6) + securerandom (0.4.1) + traces (0.18.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + zeitwerk (2.8.2) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + async + async-http + async-websocket + s2-ruby! + +BUNDLED WITH + 2.6.9 diff --git a/resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml b/resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml new file mode 100644 index 0000000..bc78447 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml @@ -0,0 +1,9 @@ +name: s2-demo-cem + +services: + cem: + build: . + environment: + PORT: "9000" + ports: + - "9000:9000" diff --git a/resource-manager-mqtt-bridge-ruby/cem/public/index.html b/resource-manager-mqtt-bridge-ruby/cem/public/index.html new file mode 100644 index 0000000..baeb068 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/public/index.html @@ -0,0 +1,150 @@ + + + + + + CEM (demo) + + + + +
+
+
S2 CEMdemo stand-in
+
The congestion controller. Sends the RM an S2 instruction to lower the load.
+
+ +
+
Waiting for the RM to connect…
+
+ +
+
+ + +
No messages yet.
+
+ + + + diff --git a/resource-manager-mqtt-bridge-ruby/cem/public/tokens.css b/resource-manager-mqtt-bridge-ruby/cem/public/tokens.css new file mode 100644 index 0000000..4fc9565 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/public/tokens.css @@ -0,0 +1,174 @@ +:root { + --bg: #eef1f5; + --panel: #ffffff; + --panel-2: #f3f6f9; + --border: #dce2e9; + --border-strong: #c5cdd7; + --text: #1b2733; + --muted: #51606f; + --faint: #6b7685; + + --brand: #0d8f61; + --brand-ink: #0a7a52; + --brand-deep: #08643f; + --brand-soft: #e6f6ef; + + --report: #2563b0; + --report-ink: #1f4f8f; + --report-soft: #e8effb; + + --command: #a85c0a; + --command-ink: #8a4c08; + --command-soft: #fbf0df; + + --setup: #6b7685; + + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --radius: 10px; + --radius-sm: 7px; + --gap: 14px; + --header-min: 84px; + --primary-min: 76px; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 14px; + -webkit-font-smoothing: antialiased; +} + +.panel { + display: flex; + flex-direction: column; + height: 100vh; + padding: var(--gap); + gap: var(--gap); + background: var(--bg); +} + +.panel-header { + display: flex; + flex-direction: column; + gap: 4px; + min-height: var(--header-min); + padding-bottom: var(--gap); + border-bottom: 1px solid var(--border); +} + +.primary { + display: flex; + flex-direction: column; + gap: 10px; + min-height: var(--primary-min); +} + +.panel-title { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.1px; + color: var(--text); +} + +.panel-role { + font-size: 12.5px; + line-height: 1.45; + color: var(--muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.panel-tag { + margin-left: 9px; + font-size: 12px; + font-weight: 550; + letter-spacing: 0; + color: var(--faint); +} + +.panel-tag.is-reference { + color: var(--text); + font-weight: 650; +} + +.section-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.7px; + font-weight: 650; + color: var(--faint); + margin: 2px 0; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.btn { + font-family: var(--sans); + font-size: 13px; + font-weight: 600; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: 8px 12px; + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; +} + +.btn:hover { border-color: var(--brand); background: var(--brand-soft); } +.btn:active { background: var(--panel-2); } +.btn:disabled { opacity: 0.55; cursor: default; } +.btn-primary { + border-color: var(--brand-ink); + background: var(--brand-ink); + color: #ffffff; +} +.btn-primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); } + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px; +} + +.feed { + flex: 1; + overflow-y: auto; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + font-size: 13px; + line-height: 1.5; +} + +.empty { color: var(--faint); font-style: italic; } + +.tstamp { + font-family: var(--mono); + font-size: 10.5px; + color: var(--faint); + white-space: nowrap; +} + +.statusline { + display: flex; + flex-wrap: wrap; + gap: 3px 20px; + font-size: 12.5px; + color: var(--muted); +} +.statusline .live { color: var(--brand-ink); font-weight: 600; } diff --git a/resource-manager-mqtt-bridge-ruby/cem/server.rb b/resource-manager-mqtt-bridge-ruby/cem/server.rb new file mode 100644 index 0000000..e8c49ce --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/cem/server.rb @@ -0,0 +1,241 @@ +require "bundler/setup" +require "json" +require "securerandom" +require "logger" +require "async" +require "async/http/server" +require "async/http/endpoint" +require "async/websocket/adapters/http" +require "s2" + +$stdout.sync = true +PORT = Integer(ENV.fetch("PORT", "9000")) +PUBLIC_DIR = File.join(__dir__, "public") +LOGGER = Logger.new($stdout) + +SESSIONS = {} + +def session_for(resource_id) + SESSIONS[resource_id] ||= { connected: false, outbound: nil, system_description: nil, control_type: nil, log: [] } +end + +def log_message(resource_id, dir, message) + log = session_for(resource_id)[:log] + log << { + dir: dir, + message_type: message["message_type"], + message_id: message["message_id"], + subject_message_id: message["subject_message_id"], + at: Time.now.utc.iso8601, + }.compact + log.shift while log.length > 50 +end + +def cem_handshake + S2::Messages::Handshake.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::HandshakeMessageType::Handshake, + "role" => S2::Messages::EnergyManagementRole::Cem, + "supported_protocol_versions" => S2.supported_protocol_versions, + ) +end + +def handshake_response + S2::Messages::HandshakeResponse.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::HandshakeResponseMessageType::HandshakeResponse, + "selected_protocol_version" => S2.supported_protocol_versions.first, + ) +end + +def select_control_type_frbc + S2::Messages::SelectControlType.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::SelectControlTypeMessageType::SelectControlType, + "control_type" => S2::Messages::ControlType::FillRateBasedControl, + ) +end + +def reception_status(subject_message_id) + S2::Messages::ReceptionStatus.from_dynamic!( + "message_type" => S2::Messages::ReceptionStatusMessageType::ReceptionStatus, + "subject_message_id" => subject_message_id, + "status" => S2::Messages::ReceptionStatusValues::Ok, + ) +end + +def frbc_instruction_from(system_description) + actuator = system_description.fetch("actuators").first + raise "FRBC.SystemDescription has no actuators" unless actuator + + modes = actuator.fetch("operation_modes") + mode = modes.find { |m| m["diagnostic_label"].to_s.match?(/charg/i) } || + modes.find { |m| !m["abnormal_condition_only"] } || + modes.first + raise "actuator has no usable operation mode" unless mode + + S2::Messages::FRBCInstruction.from_dynamic!( + "message_id" => SecureRandom.uuid, + "message_type" => S2::Messages::FRBCInstructionMessageType::FRBCInstruction, + "id" => SecureRandom.uuid, + "actuator_id" => actuator.fetch("id"), + "operation_mode" => mode.fetch("id"), + "operation_mode_factor" => 0.5, + "duration" => 600_000, + "execution_time" => (Time.now + 30).utc.iso8601, + "abnormal_condition" => false, + ) +end + +def send_s2(connection, resource_id, message) + json = message.to_json + connection.send_text(json) + connection.flush + log_message(resource_id, "out", JSON.parse(json)) +end + +def handle_connection(connection, resource_id) + token = SecureRandom.uuid + outbound = Async::Queue.new + previous = SESSIONS[resource_id] + SESSIONS[resource_id] = { + token: token, + connected: true, + outbound: outbound, + system_description: nil, + control_type: nil, + log: previous ? previous[:log] : [], + } + LOGGER.info "[cem] RM connected for resource #{resource_id}" + + writer = Async do + while (message = outbound.dequeue) + case message + when :ping + connection.send_ping + connection.flush + when :instruct + current = SESSIONS[resource_id] + sd = current && current[:token] == token ? current[:system_description] : nil + send_s2(connection, resource_id, frbc_instruction_from(sd)) if sd + else + send_s2(connection, resource_id, message) + end + end + end + + heartbeat = Async do + loop do + sleep 15 + outbound.enqueue(:ping) + end + end + + while (frame = connection.read) + message = JSON.parse(frame.to_str) + type = message["message_type"] + message_id = message["message_id"] + log_message(resource_id, "in", message) + current = SESSIONS[resource_id] + mine = current && current[:token] == token + + case type + when "ReceptionStatus" + next + when "Handshake" + outbound.enqueue(reception_status(message_id)) + outbound.enqueue(cem_handshake) + outbound.enqueue(handshake_response) + when "ResourceManagerDetails" + outbound.enqueue(reception_status(message_id)) + outbound.enqueue(select_control_type_frbc) + current[:control_type] = "FILL_RATE_BASED_CONTROL" if mine + when "FRBC.SystemDescription" + outbound.enqueue(reception_status(message_id)) + current[:system_description] = message if mine + LOGGER.info "[cem] system description cached for #{resource_id}; instruction button enabled" + else + outbound.enqueue(reception_status(message_id)) if message_id + end + end +rescue StandardError => e + LOGGER.warn "[cem] connection error for #{resource_id}: #{e.class}: #{e.message}" +ensure + current = SESSIONS[resource_id] + if current && current[:token] == token + current[:connected] = false + current[:outbound] = nil + current[:system_description] = nil + end + writer&.stop + heartbeat&.stop + LOGGER.info "[cem] RM disconnected for resource #{resource_id}" +end + +def active_session + connected = SESSIONS.select { |_id, s| s[:connected] } + connected.find { |_id, s| s[:system_description] } || connected.first || SESSIONS.first +end + +def response(status, content_type, body) + Async::HTTP::Protocol::Response[status, { "content-type" => content_type, "cache-control" => "no-store" }, [body]] +end + +def serve_static(path) + file = path == "/" ? "index.html" : path.sub(%r{\A/}, "") + full = File.expand_path(File.join(PUBLIC_DIR, file)) + return response(404, "text/plain", "not found") unless full.start_with?(PUBLIC_DIR) && File.file?(full) + + response(200, full.end_with?(".css") ? "text/css" : "text/html", File.read(full)) +end + +def state_payload + resource_id, session = active_session + { + connected: !!(session && session[:connected]), + resource_id: resource_id, + control_type: session && session[:control_type], + has_system_description: !!(session && session[:system_description]), + can_instruct: !!(session && session[:connected] && session[:system_description] && session[:outbound]), + log: session ? session[:log].last(40).reverse : [], + } +end + +def handle_instruct + resource_id, session = active_session + unless session && session[:system_description] && session[:outbound] + return response(409, "application/json", JSON.generate({ error: "no connected resource with a system description yet" })) + end + + session[:outbound].enqueue(:instruct) + response(200, "application/json", JSON.generate({ queued: "FRBC.Instruction", resource_id: resource_id })) +rescue StandardError => e + response(500, "application/json", JSON.generate({ error: e.message })) +end + +def handle_reset + SESSIONS.each_value { |session| session[:log]&.clear } + response(200, "application/json", JSON.generate({ reset: true })) +end + +def handle(request) + path = request.path.split("?").first + + if Async::WebSocket::Adapters::HTTP.websocket?(request) && path.start_with?("/s2/") + resource_id = path.split("/").reject(&:empty?).last + return Async::WebSocket::Adapters::HTTP.open(request) { |connection| handle_connection(connection, resource_id) } + end + + return handle_reset if request.method == "POST" && path == "/api/reset" + return handle_instruct if request.method == "POST" && path == "/api/instruct" + return response(200, "application/json", JSON.generate(state_payload)) if request.method == "GET" && path == "/api/state" + return serve_static(path) if request.method == "GET" + + response(405, "text/plain", "method not allowed") +end + +Async do + endpoint = Async::HTTP::Endpoint.parse("http://0.0.0.0:#{PORT}", protocol: Async::HTTP::Protocol::HTTP1) + LOGGER.info "[cem] listening on :#{PORT} (HTTP panel + S2 WebSocket server on /s2/)" + Async::HTTP::Server.new(->(request) { handle(request) }, endpoint).run +end From 7a5799c4a3f74d18ca1c060104a7af47e048dc57 Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 11 Jun 2026 15:24:26 +0200 Subject: [PATCH 04/10] Run the whole demo on one screen with one command ./demo.sh starts the broker, the CEM, the RM and the platform panel, and frames the three applications side by side on a single URL. A viewer presses one button on the MQTT side and watches the message travel through the RM to the CEM and a power setpoint come back, so the closed loop is legible from end to end. Co-authored-by: Bob Forma Co-authored-by: Erik de Bruijn Co-authored-by: Martijn Versluis --- resource-manager-mqtt-bridge-ruby/README.md | 139 +++++++++++--- resource-manager-mqtt-bridge-ruby/demo.sh | 51 +++++ .../demo/Dockerfile | 5 + .../demo/default.conf | 11 ++ .../demo/docker-compose.yml | 7 + .../demo/index.html | 112 +++++++++++ .../demo/tokens.css | 174 ++++++++++++++++++ .../docker-compose.yml | 81 ++++++++ .../docs/demo.png | Bin 0 -> 503288 bytes 9 files changed, 555 insertions(+), 25 deletions(-) create mode 100755 resource-manager-mqtt-bridge-ruby/demo.sh create mode 100644 resource-manager-mqtt-bridge-ruby/demo/Dockerfile create mode 100644 resource-manager-mqtt-bridge-ruby/demo/default.conf create mode 100644 resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml create mode 100644 resource-manager-mqtt-bridge-ruby/demo/index.html create mode 100644 resource-manager-mqtt-bridge-ruby/demo/tokens.css create mode 100644 resource-manager-mqtt-bridge-ruby/docker-compose.yml create mode 100644 resource-manager-mqtt-bridge-ruby/docs/demo.png diff --git a/resource-manager-mqtt-bridge-ruby/README.md b/resource-manager-mqtt-bridge-ruby/README.md index 9127311..b50aa95 100644 --- a/resource-manager-mqtt-bridge-ruby/README.md +++ b/resource-manager-mqtt-bridge-ruby/README.md @@ -5,11 +5,15 @@ messages, stores the resource state, and translates each message into an [S2 pro message that it relays over a WebSocket to a **CEM** (Customer Energy Manager) peer. It also turns the CEM's `FRBC.Instruction` back into a Stekker `power_setpoint` on MQTT. -The RM is the reference implementation itself. It lives in [`rm/`](rm/) and is the subject of this -document. +The RM is the reference implementation itself. It lives in [`rm/`](rm/) and is the subject of most of this +document. The last chapter, [The live three-panel demo](#the-live-three-panel-demo), shows the RM talking to +both of its neighbors at once on a single screen, using two small demo stand-ins (a CEM and an MQTT +platform) that exist only to make the RM's behavior visible. Built by [Stekker.app B.V.](https://stekker.com), the team behind the Stekker smart-charging platform. +![The live three-panel demo: the demo CEM, the RM bridge and the MQTT platform stand-in side by side, each feed showing the full S2 conversation from handshake to power setpoint](docs/demo.png) + ## What this demonstrates Each resource-side MQTT event is parsed, stored when needed, mapped to the correct S2 message type, @@ -23,7 +27,9 @@ MQTT power setpoint. - **Reverse bridge path:** an inbound S2 `FRBC.Instruction` becomes an MQTT `power_setpoint` plus an `instruction_update` acknowledgment (see [Reverse path](#reverse-path-s2--mqtt)). -You can see the forward path on its own by running the RM standalone (it falls back to a placeholder CEM). +You can see the forward path on its own by running the RM standalone (it falls back to a placeholder CEM), and +the complete closed loop by running the [three-panel demo](#the-live-three-panel-demo) where the demo CEM +is wired in. ## Roles: this app is the Resource Manager @@ -75,7 +81,8 @@ open http://localhost:3000 # read-only bridge dashboard, no logi When the RM runs standalone, there is no external CEM. The configured CEM URL is the local placeholder `ws://localhost:9999/s2`, so the app shows MQTT ingest and S2 message construction, then logs that no CEM connection is -available. That is the expected offline result, not a failure. +available. That is the expected offline result, not a failure; the [demo chapter](#the-live-three-panel-demo) +adds the demo CEM and closes the loop. The app boots cleanly on an empty database: the supervisor only connects resources that already exist, and there are none until you publish a `connect`. The dashboard has no login by default; set both `BASIC_AUTH_USERNAME` @@ -123,24 +130,6 @@ docker compose logs -f app Each `mosquitto_pub` also appears in the dashboard's "RM messages" feed at http://localhost:3000. -## Drive it from a demo charging platform - -Instead of `mosquitto_pub`, the [`mqtt/`](mqtt/) directory contains the broker and a small panel that stands -in for the real charging platform (which is not open source): one button per bundled example event, and a -feed showing the `power_setpoint` and `instruction_update` messages that come back. From `mqtt/`, run -`docker compose up --build`; the panel listens on http://localhost:4000 and the broker on port 1883. Point -the RM's `MQTT_URL` at that broker to drive the bridge from the panel. - -## Close the loop with a demo CEM - -The [`cem/`](cem/) directory holds a minimal S2 CEM counterpart: a WebSocket server that completes the -handshake (replying with its own `Handshake`, role CEM), selects `FILL_RATE_BASED_CONTROL`, acknowledges -every message with `ReceptionStatus`, and sends an `FRBC.Instruction` derived from the -`FRBC.SystemDescription` it received from the RM. From `cem/`, run `docker compose up --build`; the panel -and WebSocket server listen on http://localhost:9000. Point the RM's `CEM_BASE_URL` at `ws://:9000/s2` -and the closed loop runs on real protocol traffic: an instruction from the CEM panel comes back as a -`power_setpoint` on MQTT. - ## MQTT topics and S2 mapping Every bundled example is a real resource-side message the bridge already supports. The S2 column is the @@ -276,8 +265,9 @@ The job: 4. Publishes an `instruction_update` acknowledgment (`status: ACCEPTED`) on `stekker/resources//instruction_updates`. -The reverse path is **not exercised live** when the RM runs standalone. Below it is shown as a static, -normalized example, also covered by the in-tree spec. +The reverse path is **not exercised live** when the RM runs standalone; the +[demo chapter](#the-live-three-panel-demo) runs it end to end. Below it is shown as a static, normalized +example, also covered by the in-tree spec. Inbound S2 `FRBC.Instruction` (representative fields): @@ -335,7 +325,8 @@ against the pinned `s2-ruby` revision: ## Connecting to a real CEM The placeholder CEM is only the standalone default. You can point this RM at a real CEM, for example in an -interoperability test with another party's CEM. +interoperability test with another party's CEM. The demo CEM in [`cem/`](cem/) is exactly such a peer; the +[demo chapter](#the-live-three-panel-demo) wires it in for you. > **CEM routing requirement.** For each active resource the RM opens one WebSocket to `CEM_BASE_URL` with the > resource id appended as a trailing path segment, for example `wss://your-cem.example/s2/demo-resource-001`. @@ -454,3 +445,101 @@ Rails 8, PostgreSQL 16, Eclipse Mosquitto 2, the [Async](https://github.com/sock WebSocket I/O, Solid Queue/Cache/Cable (database-backed), and a small CSS design-token stylesheet for the read-only dashboard. +--- + +# The live three-panel demo + +To show the RM talking to both of its neighbors at once, three applications run side by side and the audience +watches S2 and MQTT messages travel left to right and back: + +``` ++----------------------+----------------------+----------------------+ +| CEM (demo) | Resource Manager | MQTT (demo) | +| | (the real one) | | ++----------------------+----------------------+----------------------+ + S2 over WebSocket <--> RM <--> MQTT (Stekker contract) +``` + +The middle panel is the RM described above. The two side panels are **demo stand-ins** that exist only to make +the RM's behavior visible. They are not products; the real Stekker smart-charging platform is not +open source. + +## Run the whole demo + +From this directory: + +```sh +./demo.sh +``` + +Then open `http://localhost:8080`. The script builds and starts the three applications plus the demo screen, waits for +the RM, and prints the URLs. Stop with `docker compose down`. + +### Walk-through + +1. In the **MQTT panel** (right), press **Connect charger** and wait until the **CEM panel** (left) shows the + Resource Manager as connected (events published before the S2 session is up are not relayed; repeating + `connect` on a live session with the same configuration is harmless). Then press **Power Measurement**: + the RM (middle) translates the event into S2, and the CEM panel shows it arriving on top of the completed + handshake. +2. In the **CEM panel**, press **Send instruction to lower the load**. The CEM sends an `FRBC.Instruction` built from + the system description the RM advertised. The RM translates it and the **MQTT panel** shows the + `power_setpoint` that comes back. + +That second step is the closed loop: a control instruction from the CEM becomes a concrete power setpoint on +the MQTT side, through the real bridge. + +### The conversation + +``` +MQTT panel --connect / power_measurement / charge_target--> RM +RM --Handshake / ResourceManagerDetails / FRBC.SystemDescription--> CEM +CEM --Handshake / HandshakeResponse / SelectControlType / FRBC.Instruction--> RM +RM --power_setpoint / instruction_update (MQTT)--> MQTT panel +``` + +Every message a panel shows is real traffic that crossed the wire. The CEM's instruction uses the +`actuator_id` and `operation_mode` from the `FRBC.SystemDescription` the RM sent. The RM also publishes a heartbeat +power measurement every quarter hour, as S2 expects, so a 0 W measurement row can appear on its own. + +## The three applications + +Each application is also a normal `docker compose up` from its own directory, useful for inspecting one in +isolation. + +| Application | What it is | Stack | +| --- | --- | --- | +| [`rm/`](rm/) | the real S2 Resource Manager (reference implementation); read-only bridge dashboard | Rails | +| [`cem/`](cem/) | demo CEM: the S2 WebSocket server the RM connects to; completes the handshake, selects `FILL_RATE_BASED_CONTROL`, caches the `FRBC.SystemDescription`, and sends a real `FRBC.Instruction` on a button press | Ruby (async, s2-ruby) | +| [`mqtt/`](mqtt/) | demo Stekker-platform stand-in; publish buttons send the bundled events, a reverse-path feed shows `power_setpoints` and `instruction_updates`; bundles the MQTT broker | Node | +| [`demo/`](demo/) | the single screen that frames the three panels side by side | nginx | + +The CEM speaks the real S2 protocol with the same pinned `s2-ruby` revision the RM uses, so the handshake and +messages are genuine, not faked. The MQTT panel publishes through its own small backend (a browser cannot +speak MQTT over TCP 1883). The demo screen holds no logic; its iframes point at browser-resolvable host URLs +(`http://localhost:9000`, `http://localhost:3000`, `http://localhost:4000`), not Docker service names. + +## Ports + +| Service | Host port | Notes | +| --- | --- | --- | +| demo screen | 8080 | the URL the presenter opens | +| RM | 3000 | the read-only bridge dashboard, framed by the demo screen | +| CEM | 9000 | HTTP panel and S2 WebSocket server (`/s2/`) | +| MQTT panel | 4000 | the platform stand-in UI | +| Mosquitto broker | 1883 | the RM connects here for MQTT | + +There is one broker for the whole demo, started by the top-level compose. The RM also ships its own broker in +`rm/docker-compose.yml` for running `rm/` on its own; the whole-demo compose does not start that one, so only +one broker binds 1883. + +If one of these host ports is taken, remap it in a local `docker-compose.override.yml` next to the top-level +compose file (the same mechanism shown for the standalone RM under [Port conflicts](#port-conflicts)); the +demo screen's iframe URLs in `demo/index.html` must then be updated to match. + +## The demo/reference boundary + +The RM ships secret-free, with secure defaults. It only allows being +embedded in the demo screen when `DEMO_ALLOW_FRAMING` is set, which the whole-demo compose does. A plain copy of +`rm/` run on its own, without that switch, keeps `X-Frame-Options: SAMEORIGIN`. The CEM and MQTT panels say +in their own UIs that they are demo stand-ins. diff --git a/resource-manager-mqtt-bridge-ruby/demo.sh b/resource-manager-mqtt-bridge-ruby/demo.sh new file mode 100755 index 0000000..65b9f68 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +REQUIRED_PORTS=(8080 3000 9000 4000 1883) +if command -v lsof >/dev/null 2>&1; then + for port in "${REQUIRED_PORTS[@]}"; do + if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then + echo "Note: port $port is already in use. If that is not this demo, 'docker compose up' will report a bind error below." >&2 + fi + done +fi + +echo "Building and starting the three applications + the demo screen (this can take a few minutes the first time)..." +docker compose up --build -d + +wait_for() { + for _ in $(seq 1 60); do + if curl -fs -o /dev/null "$1" 2>/dev/null; then + return 0 + fi + sleep 2 + done + return 1 +} + +echo "Waiting for the RM..." +if ! wait_for "http://localhost:3000/up"; then + echo "The RM did not answer on http://localhost:3000/up. Check 'docker compose logs rm'." >&2 + exit 1 +fi + +cat <<'BANNER' + +Demo is up. Open the single screen: + + http://localhost:8080 + +Panels (also reachable on their own): + CEM http://localhost:9000 + RM http://localhost:3000 + MQTT http://localhost:4000 + +Walk-through: in the MQTT panel press "Connect charger", wait until the CEM panel +shows the RM connected, then press "Power Measurement". Then in +the CEM panel press "Send instruction to lower the load" and watch a power_setpoint +arrive back in the MQTT panel. + +Stop with: docker compose down +BANNER diff --git a/resource-manager-mqtt-bridge-ruby/demo/Dockerfile b/resource-manager-mqtt-bridge-ruby/demo/Dockerfile new file mode 100644 index 0000000..08d4974 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:alpine +COPY default.conf /etc/nginx/conf.d/default.conf +COPY index.html /usr/share/nginx/html/index.html +COPY tokens.css /usr/share/nginx/html/tokens.css +EXPOSE 80 diff --git a/resource-manager-mqtt-bridge-ruby/demo/default.conf b/resource-manager-mqtt-bridge-ruby/demo/default.conf new file mode 100644 index 0000000..68ef74c --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo/default.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + etag off; + add_header Cache-Control "no-store" always; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml b/resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml new file mode 100644 index 0000000..be1d35f --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml @@ -0,0 +1,7 @@ +name: s2-demo-demo + +services: + demo: + build: . + ports: + - "8080:80" diff --git a/resource-manager-mqtt-bridge-ruby/demo/index.html b/resource-manager-mqtt-bridge-ruby/demo/index.html new file mode 100644 index 0000000..fdcd709 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo/index.html @@ -0,0 +1,112 @@ + + + + + + S2 RM to MQTT Bridge Demo + + + + +
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ + + diff --git a/resource-manager-mqtt-bridge-ruby/demo/tokens.css b/resource-manager-mqtt-bridge-ruby/demo/tokens.css new file mode 100644 index 0000000..4fc9565 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/demo/tokens.css @@ -0,0 +1,174 @@ +:root { + --bg: #eef1f5; + --panel: #ffffff; + --panel-2: #f3f6f9; + --border: #dce2e9; + --border-strong: #c5cdd7; + --text: #1b2733; + --muted: #51606f; + --faint: #6b7685; + + --brand: #0d8f61; + --brand-ink: #0a7a52; + --brand-deep: #08643f; + --brand-soft: #e6f6ef; + + --report: #2563b0; + --report-ink: #1f4f8f; + --report-soft: #e8effb; + + --command: #a85c0a; + --command-ink: #8a4c08; + --command-soft: #fbf0df; + + --setup: #6b7685; + + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --radius: 10px; + --radius-sm: 7px; + --gap: 14px; + --header-min: 84px; + --primary-min: 76px; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 14px; + -webkit-font-smoothing: antialiased; +} + +.panel { + display: flex; + flex-direction: column; + height: 100vh; + padding: var(--gap); + gap: var(--gap); + background: var(--bg); +} + +.panel-header { + display: flex; + flex-direction: column; + gap: 4px; + min-height: var(--header-min); + padding-bottom: var(--gap); + border-bottom: 1px solid var(--border); +} + +.primary { + display: flex; + flex-direction: column; + gap: 10px; + min-height: var(--primary-min); +} + +.panel-title { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.1px; + color: var(--text); +} + +.panel-role { + font-size: 12.5px; + line-height: 1.45; + color: var(--muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.panel-tag { + margin-left: 9px; + font-size: 12px; + font-weight: 550; + letter-spacing: 0; + color: var(--faint); +} + +.panel-tag.is-reference { + color: var(--text); + font-weight: 650; +} + +.section-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.7px; + font-weight: 650; + color: var(--faint); + margin: 2px 0; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.btn { + font-family: var(--sans); + font-size: 13px; + font-weight: 600; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: 8px 12px; + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; +} + +.btn:hover { border-color: var(--brand); background: var(--brand-soft); } +.btn:active { background: var(--panel-2); } +.btn:disabled { opacity: 0.55; cursor: default; } +.btn-primary { + border-color: var(--brand-ink); + background: var(--brand-ink); + color: #ffffff; +} +.btn-primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); } + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px; +} + +.feed { + flex: 1; + overflow-y: auto; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + font-size: 13px; + line-height: 1.5; +} + +.empty { color: var(--faint); font-style: italic; } + +.tstamp { + font-family: var(--mono); + font-size: 10.5px; + color: var(--faint); + white-space: nowrap; +} + +.statusline { + display: flex; + flex-wrap: wrap; + gap: 3px 20px; + font-size: 12.5px; + color: var(--muted); +} +.statusline .live { color: var(--brand-ink); font-weight: 600; } diff --git a/resource-manager-mqtt-bridge-ruby/docker-compose.yml b/resource-manager-mqtt-bridge-ruby/docker-compose.yml new file mode 100644 index 0000000..1f6772e --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/docker-compose.yml @@ -0,0 +1,81 @@ +name: s2-demo + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: resource_manager_mqtt_ruby + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d resource_manager_mqtt_ruby"] + interval: 2s + timeout: 5s + retries: 20 + volumes: + - postgres-data:/var/lib/postgresql/data + + mosquitto: + image: eclipse-mosquitto:2 + ports: + - "1883:1883" + volumes: + - ./mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + + rm: + build: ./rm + depends_on: + postgres: + condition: service_healthy + mosquitto: + condition: service_started + cem: + condition: service_healthy + environment: + RAILS_ENV: production + RAILS_LOG_LEVEL: info + SECRET_KEY_BASE: "demo-secret-key-base-for-standalone-reference-only" + DATABASE_URL: "postgres://postgres:postgres@postgres:5432/resource_manager_mqtt_ruby" + MQTT_URL: "mqtt://mosquitto:1883" + CEM_BASE_URL: "ws://cem:9000/s2" + S2_PROVIDES_FORECAST: "true" + SOLID_QUEUE_IN_PUMA: "true" + DEMO_ALLOW_FRAMING: "1" + IO_EVENT_SELECTOR: "Select" # the default epoll selector can spin at 100% CPU after an S2 session teardown; the pure-Ruby selector is immune + ports: + - "3000:3000" + + cem: + build: ./cem + environment: + PORT: "9000" + healthcheck: + test: ["CMD", "ruby", "-e", "require 'net/http'; exit(Net::HTTP.get_response(URI('http://localhost:9000/api/state')).is_a?(Net::HTTPSuccess) ? 0 : 1)"] + interval: 3s + timeout: 5s + retries: 20 + ports: + - "9000:9000" + + mqtt-panel: + build: ./mqtt + depends_on: + - mosquitto + environment: + PORT: "4000" + MQTT_URL: "mqtt://mosquitto:1883" + RESOURCE_ID: "demo-resource-001" + ports: + - "4000:4000" + + demo: + build: ./demo + depends_on: + - rm + - cem + - mqtt-panel + ports: + - "8080:80" + +volumes: + postgres-data: diff --git a/resource-manager-mqtt-bridge-ruby/docs/demo.png b/resource-manager-mqtt-bridge-ruby/docs/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb0cb91414328c100eb0b88ac330f8f8941c030 GIT binary patch literal 503288 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ξ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 z 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 literal 0 HcmV?d00001 From 6bb92c1783f4270a4f7c6b0218dfa3bc48bc87be Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse Date: Thu, 11 Jun 2026 15:53:12 +0200 Subject: [PATCH 05/10] License the directory under Apache 2.0 States the terms under which this reference implementation is offered and records copyright. The Apache 2.0 text is verbatim from the canonical source; the NOTICE file carries the Stekker.app B.V. copyright and attribution. --- resource-manager-mqtt-bridge-ruby/LICENSE | 202 ++++++++++++++++++++++ resource-manager-mqtt-bridge-ruby/NOTICE | 4 + 2 files changed, 206 insertions(+) create mode 100644 resource-manager-mqtt-bridge-ruby/LICENSE create mode 100644 resource-manager-mqtt-bridge-ruby/NOTICE diff --git a/resource-manager-mqtt-bridge-ruby/LICENSE b/resource-manager-mqtt-bridge-ruby/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resource-manager-mqtt-bridge-ruby/NOTICE b/resource-manager-mqtt-bridge-ruby/NOTICE new file mode 100644 index 0000000..03240a3 --- /dev/null +++ b/resource-manager-mqtt-bridge-ruby/NOTICE @@ -0,0 +1,4 @@ +S2 Resource Manager MQTT Bridge (Ruby) +Copyright 2026 Stekker.app B.V. + +Developed by Stekker.app B.V. (https://stekker.com). From ac22abd6aaa851756ec079ef2fae61312b32f9f7 Mon Sep 17 00:00:00 2001 From: Jorrit Nutma Date: Tue, 16 Jun 2026 15:38:26 +0200 Subject: [PATCH 06/10] Rename project folder to rm-mqtt-ws-bridge --- .../LICENSE | 0 .../NOTICE | 0 .../README.md | 0 .../cem/Dockerfile | 0 .../cem/Gemfile | 0 .../cem/Gemfile.lock | 0 .../cem/docker-compose.yml | 0 .../cem/public/index.html | 0 .../cem/public/tokens.css | 0 .../cem/server.rb | 0 .../demo.sh | 0 .../demo/Dockerfile | 0 .../demo/default.conf | 0 .../demo/docker-compose.yml | 0 .../demo/index.html | 0 .../demo/tokens.css | 0 .../docker-compose.yml | 0 .../docs/demo.png | Bin .../mqtt/Dockerfile | 0 .../mqtt/docker-compose.yml | 0 .../mqtt/examples/charge_status.json | 0 .../mqtt/examples/charge_target.json | 0 .../mqtt/examples/connect.json | 0 .../mqtt/examples/disconnect.json | 0 .../mqtt/examples/power_forecast.json | 0 .../mqtt/examples/power_measurement.json | 0 .../mqtt/mosquitto.conf | 0 .../mqtt/package.json | 0 .../mqtt/public/index.html | 0 .../mqtt/public/tokens.css | 0 .../mqtt/server.js | 0 .../rm/.dockerignore | 0 .../rm/.gitattributes | 0 .../rm/.gitignore | 0 .../rm/.rspec | 0 .../rm/.ruby-version | 0 .../rm/Dockerfile | 0 .../rm/Gemfile | 0 .../rm/Gemfile.lock | 0 .../rm/Rakefile | 0 .../rm/app/controllers/application_controller.rb | 0 .../rm/app/controllers/concerns/.keep | 0 .../rm/app/controllers/resources_controller.rb | 0 .../rm/app/controllers/traffic_controller.rb | 0 .../rm/app/helpers/application_helper.rb | 0 .../rm/app/jobs/application_job.rb | 0 .../rm/app/jobs/process_frbc_instruction_job.rb | 0 .../app/jobs/publish_s2_ready_notification_job.rb | 0 .../jobs/send_heartbeat_power_measurement_job.rb | 0 .../jobs/send_heartbeat_power_measurements_job.rb | 0 .../rm/app/lib/base_factory.rb | 0 .../rm/app/lib/connection_registry.rb | 0 .../rm/app/lib/error_notifier.rb | 0 .../rm/app/lib/frbc_system_description_factory.rb | 0 .../rm/app/lib/instruction_status_tracker.rb | 0 .../rm/app/lib/message_relayer.rb | 0 .../rm/app/lib/mqtt_message_handler.rb | 0 .../rm/app/lib/mqtt_message_logger.rb | 0 .../rm/app/lib/reference_config.rb | 0 .../rm/app/lib/resource_manager_details_factory.rb | 0 .../rm/app/lib/s2_message_handler.rb | 0 .../rm/app/lib/s2_message_logger.rb | 0 .../rm/app/lib/stekker_formatter.rb | 0 .../rm/app/lib/supervisor.rb | 0 .../rm/app/lib/traffic_log.rb | 0 .../rm/app/models/application_record.rb | 0 .../rm/app/models/concerns/.keep | 0 .../rm/app/models/resource.rb | 0 .../app/subscribers/log_mqtt_listener_subscriber.rb | 0 .../log_mqtt_message_handler_subscriber.rb | 0 .../rm/app/subscribers/log_session_subscriber.rb | 0 .../rm/app/views/layouts/bridge.html.erb | 0 .../rm/app/views/resources/index.html.erb | 0 .../rm/bin/bundle | 0 .../rm/bin/docker-entrypoint | 0 .../rm/bin/jobs | 0 .../rm/bin/rails | 0 .../rm/bin/rake | 0 .../rm/config.ru | 0 .../rm/config/application.rb | 0 .../rm/config/boot.rb | 0 .../rm/config/cable.yml | 0 .../rm/config/cache.yml | 0 .../rm/config/database.yml | 0 .../rm/config/environment.rb | 0 .../rm/config/environments/development.rb | 0 .../rm/config/environments/production.rb | 0 .../rm/config/environments/test.rb | 0 .../rm/config/initializers/assets.rb | 0 .../config/initializers/content_security_policy.rb | 0 .../config/initializers/filter_parameter_logging.rb | 0 .../rm/config/initializers/inflections.rb | 0 .../rm/config/initializers/instrumentation.rb | 0 .../initializers/new_framework_defaults_8_1.rb | 0 .../rm/config/initializers/s2.rb | 0 .../rm/config/puma.rb | 0 .../rm/config/queue.yml | 0 .../rm/config/recurring.yml | 0 .../rm/config/routes.rb | 0 .../db/migrate/20250908135623_create_resources.rb | 0 .../db/migrate/20250923094344_setup_solid_cable.rb | 0 .../db/migrate/20250923094447_setup_solid_queue.rb | 0 .../db/migrate/20250923094521_setup_solid_cache.rb | 0 ...0130_remove_stekker_identifier_from_resources.rb | 0 .../20251002095142_add_configuration_to_resource.rb | 0 ...0437_add_uniqueness_to_resource_s2_identifier.rb | 0 ...0251002110554_add_disconnected_at_to_resource.rb | 0 ...37_add_last_power_measurement_at_to_resources.rb | 0 .../rm/db/schema.rb | 0 .../rm/db/seeds.rb | 0 .../rm/docker-compose.yml | 0 .../rm/examples/charge_status.json | 0 .../rm/examples/charge_target.json | 0 .../rm/examples/connect.json | 0 .../rm/examples/disconnect.json | 0 .../rm/examples/instruction_update.json | 0 .../rm/examples/power_forecast.json | 0 .../rm/examples/power_measurement.json | 0 .../rm/lib/tasks/.keep | 0 .../rm/log/.keep | 0 .../rm/mosquitto.conf | 0 .../rm/public/400.html | 0 .../rm/public/404.html | 0 .../rm/public/406-unsupported-browser.html | 0 .../rm/public/422.html | 0 .../rm/public/500.html | 0 .../rm/public/icon.png | Bin .../rm/public/icon.svg | 0 .../rm/public/robots.txt | 0 .../rm/public/tokens.css | 0 .../rm/script/.keep | 0 .../rm/spec/factories/resources.rb | 0 .../spec/factories/s2/messages/frbc/instruction.rb | 0 .../rm/spec/factories/s2/messages/handshake.rb | 0 .../factories/s2/messages/handshake_response.rb | 0 .../spec/factories/s2/messages/reception_status.rb | 0 .../factories/s2/messages/select_control_type.rb | 0 .../rm/spec/factories/uuid.rb | 0 .../spec/jobs/process_frbc_instruction_job_spec.rb | 0 .../jobs/publish_s2_ready_notification_job_spec.rb | 0 .../send_heartbeat_power_measurement_job_spec.rb | 0 .../send_heartbeat_power_measurements_job_spec.rb | 0 .../rm/spec/lib/base_factory_spec.rb | 0 .../rm/spec/lib/error_notifier_spec.rb | 0 .../rm/spec/lib/instruction_status_tracker_spec.rb | 0 .../rm/spec/lib/message_relayer_spec.rb | 0 .../rm/spec/lib/mqtt_message_handler_spec.rb | 0 .../rm/spec/lib/mqtt_message_logger_spec.rb | 0 .../rm/spec/lib/s2/connection_spec.rb | 0 .../rm/spec/lib/s2/message_factory_spec.rb | 0 .../spec/lib/s2/message_handler/dispatching_spec.rb | 0 .../lib/s2/message_handler/error_handling_spec.rb | 0 .../rm/spec/lib/s2/message_handler/replying_spec.rb | 0 .../rm/spec/lib/s2/message_handler_spec.rb | 0 .../rm/spec/lib/s2/message_sender_spec.rb | 0 .../rm/spec/lib/s2/session_spec.rb | 0 .../rm/spec/lib/s2_message_handler_spec.rb | 0 .../rm/spec/lib/s2_message_logger_spec.rb | 0 .../rm/spec/lib/supervisor_spec.rb | 0 .../rm/spec/lib/traffic_log_spec.rb | 0 .../rm/spec/models/resource_spec.rb | 0 .../rm/spec/rails_helper.rb | 0 .../rm/spec/requests/resources_spec.rb | 0 .../rm/spec/requests/traffic_spec.rb | 0 .../rm/spec/stekker_s2/application_spec.rb | 0 .../log_mqtt_listener_subscriber_spec.rb | 0 .../log_mqtt_message_handler_subscriber_spec.rb | 0 .../spec/subscribers/log_session_subscriber_spec.rb | 0 .../rm/spec/support/async.rb | 0 .../rm/spec/support/factory_bot.rb | 0 .../rm/spec/support/fake_mqtt_client.rb | 0 .../rm/spec/support/fake_websocket.rb | 0 .../rm/spec/support/message_sender_stub.rb | 0 .../rm/spec/support/shoulda_matchers.rb | 0 .../rm/spec/support/websocket_server.rb | 0 .../rm/vendor/.keep | 0 .../rm/vendor/stekker-mqtt/.gitignore | 0 .../rm/vendor/stekker-mqtt/.rspec | 0 .../rm/vendor/stekker-mqtt/.ruby-version | 0 .../rm/vendor/stekker-mqtt/Gemfile | 0 .../rm/vendor/stekker-mqtt/Gemfile.lock | 0 .../rm/vendor/stekker-mqtt/LICENSE.txt | 0 .../rm/vendor/stekker-mqtt/README.md | 0 .../rm/vendor/stekker-mqtt/Rakefile | 0 .../rm/vendor/stekker-mqtt/bin/console | 0 .../rm/vendor/stekker-mqtt/bin/generate_messages | 0 .../rm/vendor/stekker-mqtt/bin/setup | 0 .../lib/json-schema/charge_status.schema.json | 0 .../lib/json-schema/charge_target.schema.json | 0 .../stekker-mqtt/lib/json-schema/common.schema.json | 0 .../lib/json-schema/connect.schema.json | 0 .../lib/json-schema/disconnect.schema.json | 0 .../lib/json-schema/instruction_update.schema.json | 0 .../lib/json-schema/power_forecast.schema.json | 0 .../lib/json-schema/power_measurement.schema.json | 0 .../lib/json-schema/power_setpoint.schema.json | 0 .../json-schema/s2_ready_notification.schema.json | 0 .../rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/factories.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/fake_client.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/listener.rb | 0 .../lib/stekker/mqtt/message_factory.rb | 0 .../lib/stekker/mqtt/message_handler.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/messages.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/publisher.rb | 0 .../stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb | 0 .../vendor/stekker-mqtt/lib/stekker/mqtt/version.rb | 0 .../stekker-mqtt/spec/factories/charge_status.rb | 0 .../stekker-mqtt/spec/factories/charge_target.rb | 0 .../vendor/stekker-mqtt/spec/factories/connect.rb | 0 .../stekker-mqtt/spec/factories/disconnect.rb | 0 .../spec/factories/instruction_update.rb | 0 .../stekker-mqtt/spec/factories/power_forecast.rb | 0 .../spec/factories/power_measurement.rb | 0 .../stekker-mqtt/spec/factories/power_setpoint.rb | 0 .../spec/factories/s2_ready_notification.rb | 0 .../rm/vendor/stekker-mqtt/spec/spec_helper.rb | 0 .../stekker-mqtt/spec/stekker/mqtt/listener_spec.rb | 0 .../spec/stekker/mqtt/message_factory_spec.rb | 0 .../spec/stekker/mqtt/message_handler_spec.rb | 0 .../spec/stekker/mqtt/publisher_spec.rb | 0 .../vendor/stekker-mqtt/spec/support/factory_bot.rb | 0 .../rm/vendor/stekker-mqtt/stekker-mqtt.gemspec | 0 223 files changed, 0 insertions(+), 0 deletions(-) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/LICENSE (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/NOTICE (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/README.md (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/Dockerfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/Gemfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/Gemfile.lock (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/docker-compose.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/public/index.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/public/tokens.css (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/cem/server.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo.sh (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo/Dockerfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo/default.conf (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo/docker-compose.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo/index.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/demo/tokens.css (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/docker-compose.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/docs/demo.png (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/Dockerfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/docker-compose.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/charge_status.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/charge_target.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/connect.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/disconnect.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/power_forecast.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/examples/power_measurement.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/mosquitto.conf (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/package.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/public/index.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/public/tokens.css (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/mqtt/server.js (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/.dockerignore (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/.gitattributes (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/.gitignore (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/.rspec (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/.ruby-version (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/Dockerfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/Gemfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/Gemfile.lock (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/Rakefile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/controllers/application_controller.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/controllers/concerns/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/controllers/resources_controller.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/controllers/traffic_controller.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/helpers/application_helper.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/jobs/application_job.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/jobs/process_frbc_instruction_job.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/jobs/publish_s2_ready_notification_job.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/jobs/send_heartbeat_power_measurement_job.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/jobs/send_heartbeat_power_measurements_job.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/base_factory.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/connection_registry.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/error_notifier.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/frbc_system_description_factory.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/instruction_status_tracker.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/message_relayer.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/mqtt_message_handler.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/mqtt_message_logger.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/reference_config.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/resource_manager_details_factory.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/s2_message_handler.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/s2_message_logger.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/stekker_formatter.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/supervisor.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/lib/traffic_log.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/models/application_record.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/models/concerns/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/models/resource.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/subscribers/log_mqtt_listener_subscriber.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/subscribers/log_session_subscriber.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/views/layouts/bridge.html.erb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/app/views/resources/index.html.erb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/bin/bundle (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/bin/docker-entrypoint (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/bin/jobs (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/bin/rails (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/bin/rake (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config.ru (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/application.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/boot.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/cable.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/cache.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/database.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/environment.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/environments/development.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/environments/production.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/environments/test.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/assets.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/content_security_policy.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/filter_parameter_logging.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/inflections.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/instrumentation.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/new_framework_defaults_8_1.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/initializers/s2.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/puma.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/queue.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/recurring.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/config/routes.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20250908135623_create_resources.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20250923094344_setup_solid_cable.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20250923094447_setup_solid_queue.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20250923094521_setup_solid_cache.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20251002095142_add_configuration_to_resource.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/schema.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/db/seeds.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/docker-compose.yml (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/charge_status.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/charge_target.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/connect.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/disconnect.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/instruction_update.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/power_forecast.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/examples/power_measurement.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/lib/tasks/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/log/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/mosquitto.conf (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/400.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/404.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/406-unsupported-browser.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/422.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/500.html (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/icon.png (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/icon.svg (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/robots.txt (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/public/tokens.css (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/script/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/resources.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/s2/messages/frbc/instruction.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/s2/messages/handshake.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/s2/messages/handshake_response.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/s2/messages/reception_status.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/s2/messages/select_control_type.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/factories/uuid.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/jobs/process_frbc_instruction_job_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/base_factory_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/error_notifier_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/instruction_status_tracker_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/message_relayer_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/mqtt_message_handler_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/mqtt_message_logger_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/connection_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_factory_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_handler/dispatching_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_handler/error_handling_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_handler/replying_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_handler_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/message_sender_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2/session_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2_message_handler_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/s2_message_logger_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/supervisor_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/lib/traffic_log_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/models/resource_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/rails_helper.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/requests/resources_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/requests/traffic_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/stekker_s2/application_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/subscribers/log_session_subscriber_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/async.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/factory_bot.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/fake_mqtt_client.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/fake_websocket.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/message_sender_stub.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/shoulda_matchers.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/spec/support/websocket_server.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/.keep (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/.gitignore (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/.rspec (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/.ruby-version (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/Gemfile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/Gemfile.lock (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/LICENSE.txt (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/README.md (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/Rakefile (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/bin/console (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/bin/generate_messages (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/bin/setup (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/connect.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/spec_helper.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb (100%) rename {resource-manager-mqtt-bridge-ruby => rm-mqtt-ws-bridge}/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec (100%) diff --git a/resource-manager-mqtt-bridge-ruby/LICENSE b/rm-mqtt-ws-bridge/LICENSE similarity index 100% rename from resource-manager-mqtt-bridge-ruby/LICENSE rename to rm-mqtt-ws-bridge/LICENSE diff --git a/resource-manager-mqtt-bridge-ruby/NOTICE b/rm-mqtt-ws-bridge/NOTICE similarity index 100% rename from resource-manager-mqtt-bridge-ruby/NOTICE rename to rm-mqtt-ws-bridge/NOTICE diff --git a/resource-manager-mqtt-bridge-ruby/README.md b/rm-mqtt-ws-bridge/README.md similarity index 100% rename from resource-manager-mqtt-bridge-ruby/README.md rename to rm-mqtt-ws-bridge/README.md diff --git a/resource-manager-mqtt-bridge-ruby/cem/Dockerfile b/rm-mqtt-ws-bridge/cem/Dockerfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/Dockerfile rename to rm-mqtt-ws-bridge/cem/Dockerfile diff --git a/resource-manager-mqtt-bridge-ruby/cem/Gemfile b/rm-mqtt-ws-bridge/cem/Gemfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/Gemfile rename to rm-mqtt-ws-bridge/cem/Gemfile diff --git a/resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock b/rm-mqtt-ws-bridge/cem/Gemfile.lock similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/Gemfile.lock rename to rm-mqtt-ws-bridge/cem/Gemfile.lock diff --git a/resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml b/rm-mqtt-ws-bridge/cem/docker-compose.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/docker-compose.yml rename to rm-mqtt-ws-bridge/cem/docker-compose.yml diff --git a/resource-manager-mqtt-bridge-ruby/cem/public/index.html b/rm-mqtt-ws-bridge/cem/public/index.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/public/index.html rename to rm-mqtt-ws-bridge/cem/public/index.html diff --git a/resource-manager-mqtt-bridge-ruby/cem/public/tokens.css b/rm-mqtt-ws-bridge/cem/public/tokens.css similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/public/tokens.css rename to rm-mqtt-ws-bridge/cem/public/tokens.css diff --git a/resource-manager-mqtt-bridge-ruby/cem/server.rb b/rm-mqtt-ws-bridge/cem/server.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/cem/server.rb rename to rm-mqtt-ws-bridge/cem/server.rb diff --git a/resource-manager-mqtt-bridge-ruby/demo.sh b/rm-mqtt-ws-bridge/demo.sh similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo.sh rename to rm-mqtt-ws-bridge/demo.sh diff --git a/resource-manager-mqtt-bridge-ruby/demo/Dockerfile b/rm-mqtt-ws-bridge/demo/Dockerfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo/Dockerfile rename to rm-mqtt-ws-bridge/demo/Dockerfile diff --git a/resource-manager-mqtt-bridge-ruby/demo/default.conf b/rm-mqtt-ws-bridge/demo/default.conf similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo/default.conf rename to rm-mqtt-ws-bridge/demo/default.conf diff --git a/resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml b/rm-mqtt-ws-bridge/demo/docker-compose.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo/docker-compose.yml rename to rm-mqtt-ws-bridge/demo/docker-compose.yml diff --git a/resource-manager-mqtt-bridge-ruby/demo/index.html b/rm-mqtt-ws-bridge/demo/index.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo/index.html rename to rm-mqtt-ws-bridge/demo/index.html diff --git a/resource-manager-mqtt-bridge-ruby/demo/tokens.css b/rm-mqtt-ws-bridge/demo/tokens.css similarity index 100% rename from resource-manager-mqtt-bridge-ruby/demo/tokens.css rename to rm-mqtt-ws-bridge/demo/tokens.css diff --git a/resource-manager-mqtt-bridge-ruby/docker-compose.yml b/rm-mqtt-ws-bridge/docker-compose.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/docker-compose.yml rename to rm-mqtt-ws-bridge/docker-compose.yml diff --git a/resource-manager-mqtt-bridge-ruby/docs/demo.png b/rm-mqtt-ws-bridge/docs/demo.png similarity index 100% rename from resource-manager-mqtt-bridge-ruby/docs/demo.png rename to rm-mqtt-ws-bridge/docs/demo.png diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile b/rm-mqtt-ws-bridge/mqtt/Dockerfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/Dockerfile rename to rm-mqtt-ws-bridge/mqtt/Dockerfile diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml b/rm-mqtt-ws-bridge/mqtt/docker-compose.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/docker-compose.yml rename to rm-mqtt-ws-bridge/mqtt/docker-compose.yml diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json b/rm-mqtt-ws-bridge/mqtt/examples/charge_status.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_status.json rename to rm-mqtt-ws-bridge/mqtt/examples/charge_status.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json b/rm-mqtt-ws-bridge/mqtt/examples/charge_target.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/charge_target.json rename to rm-mqtt-ws-bridge/mqtt/examples/charge_target.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json b/rm-mqtt-ws-bridge/mqtt/examples/connect.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/connect.json rename to rm-mqtt-ws-bridge/mqtt/examples/connect.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json b/rm-mqtt-ws-bridge/mqtt/examples/disconnect.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/disconnect.json rename to rm-mqtt-ws-bridge/mqtt/examples/disconnect.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json b/rm-mqtt-ws-bridge/mqtt/examples/power_forecast.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/power_forecast.json rename to rm-mqtt-ws-bridge/mqtt/examples/power_forecast.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json b/rm-mqtt-ws-bridge/mqtt/examples/power_measurement.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/examples/power_measurement.json rename to rm-mqtt-ws-bridge/mqtt/examples/power_measurement.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf b/rm-mqtt-ws-bridge/mqtt/mosquitto.conf similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/mosquitto.conf rename to rm-mqtt-ws-bridge/mqtt/mosquitto.conf diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/package.json b/rm-mqtt-ws-bridge/mqtt/package.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/package.json rename to rm-mqtt-ws-bridge/mqtt/package.json diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/public/index.html b/rm-mqtt-ws-bridge/mqtt/public/index.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/public/index.html rename to rm-mqtt-ws-bridge/mqtt/public/index.html diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css b/rm-mqtt-ws-bridge/mqtt/public/tokens.css similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/public/tokens.css rename to rm-mqtt-ws-bridge/mqtt/public/tokens.css diff --git a/resource-manager-mqtt-bridge-ruby/mqtt/server.js b/rm-mqtt-ws-bridge/mqtt/server.js similarity index 100% rename from resource-manager-mqtt-bridge-ruby/mqtt/server.js rename to rm-mqtt-ws-bridge/mqtt/server.js diff --git a/resource-manager-mqtt-bridge-ruby/rm/.dockerignore b/rm-mqtt-ws-bridge/rm/.dockerignore similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/.dockerignore rename to rm-mqtt-ws-bridge/rm/.dockerignore diff --git a/resource-manager-mqtt-bridge-ruby/rm/.gitattributes b/rm-mqtt-ws-bridge/rm/.gitattributes similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/.gitattributes rename to rm-mqtt-ws-bridge/rm/.gitattributes diff --git a/resource-manager-mqtt-bridge-ruby/rm/.gitignore b/rm-mqtt-ws-bridge/rm/.gitignore similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/.gitignore rename to rm-mqtt-ws-bridge/rm/.gitignore diff --git a/resource-manager-mqtt-bridge-ruby/rm/.rspec b/rm-mqtt-ws-bridge/rm/.rspec similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/.rspec rename to rm-mqtt-ws-bridge/rm/.rspec diff --git a/resource-manager-mqtt-bridge-ruby/rm/.ruby-version b/rm-mqtt-ws-bridge/rm/.ruby-version similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/.ruby-version rename to rm-mqtt-ws-bridge/rm/.ruby-version diff --git a/resource-manager-mqtt-bridge-ruby/rm/Dockerfile b/rm-mqtt-ws-bridge/rm/Dockerfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/Dockerfile rename to rm-mqtt-ws-bridge/rm/Dockerfile diff --git a/resource-manager-mqtt-bridge-ruby/rm/Gemfile b/rm-mqtt-ws-bridge/rm/Gemfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/Gemfile rename to rm-mqtt-ws-bridge/rm/Gemfile diff --git a/resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock b/rm-mqtt-ws-bridge/rm/Gemfile.lock similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/Gemfile.lock rename to rm-mqtt-ws-bridge/rm/Gemfile.lock diff --git a/resource-manager-mqtt-bridge-ruby/rm/Rakefile b/rm-mqtt-ws-bridge/rm/Rakefile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/Rakefile rename to rm-mqtt-ws-bridge/rm/Rakefile diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb b/rm-mqtt-ws-bridge/rm/app/controllers/application_controller.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/controllers/application_controller.rb rename to rm-mqtt-ws-bridge/rm/app/controllers/application_controller.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/concerns/.keep b/rm-mqtt-ws-bridge/rm/app/controllers/concerns/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/controllers/concerns/.keep rename to rm-mqtt-ws-bridge/rm/app/controllers/concerns/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb b/rm-mqtt-ws-bridge/rm/app/controllers/resources_controller.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/controllers/resources_controller.rb rename to rm-mqtt-ws-bridge/rm/app/controllers/resources_controller.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb b/rm-mqtt-ws-bridge/rm/app/controllers/traffic_controller.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/controllers/traffic_controller.rb rename to rm-mqtt-ws-bridge/rm/app/controllers/traffic_controller.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb b/rm-mqtt-ws-bridge/rm/app/helpers/application_helper.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/helpers/application_helper.rb rename to rm-mqtt-ws-bridge/rm/app/helpers/application_helper.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/application_job.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/jobs/application_job.rb rename to rm-mqtt-ws-bridge/rm/app/jobs/application_job.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/jobs/process_frbc_instruction_job.rb rename to rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/publish_s2_ready_notification_job.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/jobs/publish_s2_ready_notification_job.rb rename to rm-mqtt-ws-bridge/rm/app/jobs/publish_s2_ready_notification_job.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/send_heartbeat_power_measurement_job.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurement_job.rb rename to rm-mqtt-ws-bridge/rm/app/jobs/send_heartbeat_power_measurement_job.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/send_heartbeat_power_measurements_job.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/jobs/send_heartbeat_power_measurements_job.rb rename to rm-mqtt-ws-bridge/rm/app/jobs/send_heartbeat_power_measurements_job.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb b/rm-mqtt-ws-bridge/rm/app/lib/base_factory.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/base_factory.rb rename to rm-mqtt-ws-bridge/rm/app/lib/base_factory.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb b/rm-mqtt-ws-bridge/rm/app/lib/connection_registry.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/connection_registry.rb rename to rm-mqtt-ws-bridge/rm/app/lib/connection_registry.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb b/rm-mqtt-ws-bridge/rm/app/lib/error_notifier.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/error_notifier.rb rename to rm-mqtt-ws-bridge/rm/app/lib/error_notifier.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb b/rm-mqtt-ws-bridge/rm/app/lib/frbc_system_description_factory.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/frbc_system_description_factory.rb rename to rm-mqtt-ws-bridge/rm/app/lib/frbc_system_description_factory.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb b/rm-mqtt-ws-bridge/rm/app/lib/instruction_status_tracker.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/instruction_status_tracker.rb rename to rm-mqtt-ws-bridge/rm/app/lib/instruction_status_tracker.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb b/rm-mqtt-ws-bridge/rm/app/lib/message_relayer.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/message_relayer.rb rename to rm-mqtt-ws-bridge/rm/app/lib/message_relayer.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb b/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_handler.rb rename to rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb b/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_logger.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/mqtt_message_logger.rb rename to rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_logger.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb b/rm-mqtt-ws-bridge/rm/app/lib/reference_config.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/reference_config.rb rename to rm-mqtt-ws-bridge/rm/app/lib/reference_config.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb b/rm-mqtt-ws-bridge/rm/app/lib/resource_manager_details_factory.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/resource_manager_details_factory.rb rename to rm-mqtt-ws-bridge/rm/app/lib/resource_manager_details_factory.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb b/rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_handler.rb rename to rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb b/rm-mqtt-ws-bridge/rm/app/lib/s2_message_logger.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/s2_message_logger.rb rename to rm-mqtt-ws-bridge/rm/app/lib/s2_message_logger.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb b/rm-mqtt-ws-bridge/rm/app/lib/stekker_formatter.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/stekker_formatter.rb rename to rm-mqtt-ws-bridge/rm/app/lib/stekker_formatter.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb b/rm-mqtt-ws-bridge/rm/app/lib/supervisor.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/supervisor.rb rename to rm-mqtt-ws-bridge/rm/app/lib/supervisor.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb b/rm-mqtt-ws-bridge/rm/app/lib/traffic_log.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/lib/traffic_log.rb rename to rm-mqtt-ws-bridge/rm/app/lib/traffic_log.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb b/rm-mqtt-ws-bridge/rm/app/models/application_record.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/models/application_record.rb rename to rm-mqtt-ws-bridge/rm/app/models/application_record.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/concerns/.keep b/rm-mqtt-ws-bridge/rm/app/models/concerns/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/models/concerns/.keep rename to rm-mqtt-ws-bridge/rm/app/models/concerns/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb b/rm-mqtt-ws-bridge/rm/app/models/resource.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/models/resource.rb rename to rm-mqtt-ws-bridge/rm/app/models/resource.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb b/rm-mqtt-ws-bridge/rm/app/subscribers/log_mqtt_listener_subscriber.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_listener_subscriber.rb rename to rm-mqtt-ws-bridge/rm/app/subscribers/log_mqtt_listener_subscriber.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb b/rm-mqtt-ws-bridge/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb rename to rm-mqtt-ws-bridge/rm/app/subscribers/log_mqtt_message_handler_subscriber.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb b/rm-mqtt-ws-bridge/rm/app/subscribers/log_session_subscriber.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/subscribers/log_session_subscriber.rb rename to rm-mqtt-ws-bridge/rm/app/subscribers/log_session_subscriber.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb b/rm-mqtt-ws-bridge/rm/app/views/layouts/bridge.html.erb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/views/layouts/bridge.html.erb rename to rm-mqtt-ws-bridge/rm/app/views/layouts/bridge.html.erb diff --git a/resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb b/rm-mqtt-ws-bridge/rm/app/views/resources/index.html.erb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/app/views/resources/index.html.erb rename to rm-mqtt-ws-bridge/rm/app/views/resources/index.html.erb diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/bundle b/rm-mqtt-ws-bridge/rm/bin/bundle similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/bin/bundle rename to rm-mqtt-ws-bridge/rm/bin/bundle diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint b/rm-mqtt-ws-bridge/rm/bin/docker-entrypoint similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/bin/docker-entrypoint rename to rm-mqtt-ws-bridge/rm/bin/docker-entrypoint diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/jobs b/rm-mqtt-ws-bridge/rm/bin/jobs similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/bin/jobs rename to rm-mqtt-ws-bridge/rm/bin/jobs diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/rails b/rm-mqtt-ws-bridge/rm/bin/rails similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/bin/rails rename to rm-mqtt-ws-bridge/rm/bin/rails diff --git a/resource-manager-mqtt-bridge-ruby/rm/bin/rake b/rm-mqtt-ws-bridge/rm/bin/rake similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/bin/rake rename to rm-mqtt-ws-bridge/rm/bin/rake diff --git a/resource-manager-mqtt-bridge-ruby/rm/config.ru b/rm-mqtt-ws-bridge/rm/config.ru similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config.ru rename to rm-mqtt-ws-bridge/rm/config.ru diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/application.rb b/rm-mqtt-ws-bridge/rm/config/application.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/application.rb rename to rm-mqtt-ws-bridge/rm/config/application.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/boot.rb b/rm-mqtt-ws-bridge/rm/config/boot.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/boot.rb rename to rm-mqtt-ws-bridge/rm/config/boot.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/cable.yml b/rm-mqtt-ws-bridge/rm/config/cable.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/cable.yml rename to rm-mqtt-ws-bridge/rm/config/cable.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/cache.yml b/rm-mqtt-ws-bridge/rm/config/cache.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/cache.yml rename to rm-mqtt-ws-bridge/rm/config/cache.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/database.yml b/rm-mqtt-ws-bridge/rm/config/database.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/database.yml rename to rm-mqtt-ws-bridge/rm/config/database.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environment.rb b/rm-mqtt-ws-bridge/rm/config/environment.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/environment.rb rename to rm-mqtt-ws-bridge/rm/config/environment.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb b/rm-mqtt-ws-bridge/rm/config/environments/development.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/environments/development.rb rename to rm-mqtt-ws-bridge/rm/config/environments/development.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb b/rm-mqtt-ws-bridge/rm/config/environments/production.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/environments/production.rb rename to rm-mqtt-ws-bridge/rm/config/environments/production.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb b/rm-mqtt-ws-bridge/rm/config/environments/test.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/environments/test.rb rename to rm-mqtt-ws-bridge/rm/config/environments/test.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb b/rm-mqtt-ws-bridge/rm/config/initializers/assets.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/assets.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/assets.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb b/rm-mqtt-ws-bridge/rm/config/initializers/content_security_policy.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/content_security_policy.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/content_security_policy.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb b/rm-mqtt-ws-bridge/rm/config/initializers/filter_parameter_logging.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/filter_parameter_logging.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/filter_parameter_logging.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb b/rm-mqtt-ws-bridge/rm/config/initializers/inflections.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/inflections.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/inflections.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb b/rm-mqtt-ws-bridge/rm/config/initializers/instrumentation.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/instrumentation.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/instrumentation.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb b/rm-mqtt-ws-bridge/rm/config/initializers/new_framework_defaults_8_1.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/new_framework_defaults_8_1.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/new_framework_defaults_8_1.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb b/rm-mqtt-ws-bridge/rm/config/initializers/s2.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/initializers/s2.rb rename to rm-mqtt-ws-bridge/rm/config/initializers/s2.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/puma.rb b/rm-mqtt-ws-bridge/rm/config/puma.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/puma.rb rename to rm-mqtt-ws-bridge/rm/config/puma.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/queue.yml b/rm-mqtt-ws-bridge/rm/config/queue.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/queue.yml rename to rm-mqtt-ws-bridge/rm/config/queue.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml b/rm-mqtt-ws-bridge/rm/config/recurring.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/recurring.yml rename to rm-mqtt-ws-bridge/rm/config/recurring.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/config/routes.rb b/rm-mqtt-ws-bridge/rm/config/routes.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/config/routes.rb rename to rm-mqtt-ws-bridge/rm/config/routes.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20250908135623_create_resources.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250908135623_create_resources.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20250908135623_create_resources.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20250923094344_setup_solid_cable.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094344_setup_solid_cable.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20250923094344_setup_solid_cable.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20250923094447_setup_solid_queue.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094447_setup_solid_queue.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20250923094447_setup_solid_queue.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20250923094521_setup_solid_cache.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923094521_setup_solid_cache.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20250923094521_setup_solid_cache.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20250923120130_remove_stekker_identifier_from_resources.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20251002095142_add_configuration_to_resource.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002095142_add_configuration_to_resource.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20251002095142_add_configuration_to_resource.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20251002100437_add_uniqueness_to_resource_s2_identifier.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20251002110554_add_disconnected_at_to_resource.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb b/rm-mqtt-ws-bridge/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb rename to rm-mqtt-ws-bridge/rm/db/migrate/20260303155137_add_last_power_measurement_at_to_resources.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/schema.rb b/rm-mqtt-ws-bridge/rm/db/schema.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/schema.rb rename to rm-mqtt-ws-bridge/rm/db/schema.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/db/seeds.rb b/rm-mqtt-ws-bridge/rm/db/seeds.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/db/seeds.rb rename to rm-mqtt-ws-bridge/rm/db/seeds.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml b/rm-mqtt-ws-bridge/rm/docker-compose.yml similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/docker-compose.yml rename to rm-mqtt-ws-bridge/rm/docker-compose.yml diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json b/rm-mqtt-ws-bridge/rm/examples/charge_status.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/charge_status.json rename to rm-mqtt-ws-bridge/rm/examples/charge_status.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json b/rm-mqtt-ws-bridge/rm/examples/charge_target.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/charge_target.json rename to rm-mqtt-ws-bridge/rm/examples/charge_target.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/connect.json b/rm-mqtt-ws-bridge/rm/examples/connect.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/connect.json rename to rm-mqtt-ws-bridge/rm/examples/connect.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json b/rm-mqtt-ws-bridge/rm/examples/disconnect.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/disconnect.json rename to rm-mqtt-ws-bridge/rm/examples/disconnect.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json b/rm-mqtt-ws-bridge/rm/examples/instruction_update.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/instruction_update.json rename to rm-mqtt-ws-bridge/rm/examples/instruction_update.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json b/rm-mqtt-ws-bridge/rm/examples/power_forecast.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/power_forecast.json rename to rm-mqtt-ws-bridge/rm/examples/power_forecast.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json b/rm-mqtt-ws-bridge/rm/examples/power_measurement.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/examples/power_measurement.json rename to rm-mqtt-ws-bridge/rm/examples/power_measurement.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/lib/tasks/.keep b/rm-mqtt-ws-bridge/rm/lib/tasks/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/lib/tasks/.keep rename to rm-mqtt-ws-bridge/rm/lib/tasks/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/log/.keep b/rm-mqtt-ws-bridge/rm/log/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/log/.keep rename to rm-mqtt-ws-bridge/rm/log/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf b/rm-mqtt-ws-bridge/rm/mosquitto.conf similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/mosquitto.conf rename to rm-mqtt-ws-bridge/rm/mosquitto.conf diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/400.html b/rm-mqtt-ws-bridge/rm/public/400.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/400.html rename to rm-mqtt-ws-bridge/rm/public/400.html diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/404.html b/rm-mqtt-ws-bridge/rm/public/404.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/404.html rename to rm-mqtt-ws-bridge/rm/public/404.html diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html b/rm-mqtt-ws-bridge/rm/public/406-unsupported-browser.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/406-unsupported-browser.html rename to rm-mqtt-ws-bridge/rm/public/406-unsupported-browser.html diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/422.html b/rm-mqtt-ws-bridge/rm/public/422.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/422.html rename to rm-mqtt-ws-bridge/rm/public/422.html diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/500.html b/rm-mqtt-ws-bridge/rm/public/500.html similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/500.html rename to rm-mqtt-ws-bridge/rm/public/500.html diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/icon.png b/rm-mqtt-ws-bridge/rm/public/icon.png similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/icon.png rename to rm-mqtt-ws-bridge/rm/public/icon.png diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/icon.svg b/rm-mqtt-ws-bridge/rm/public/icon.svg similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/icon.svg rename to rm-mqtt-ws-bridge/rm/public/icon.svg diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/robots.txt b/rm-mqtt-ws-bridge/rm/public/robots.txt similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/robots.txt rename to rm-mqtt-ws-bridge/rm/public/robots.txt diff --git a/resource-manager-mqtt-bridge-ruby/rm/public/tokens.css b/rm-mqtt-ws-bridge/rm/public/tokens.css similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/public/tokens.css rename to rm-mqtt-ws-bridge/rm/public/tokens.css diff --git a/resource-manager-mqtt-bridge-ruby/rm/script/.keep b/rm-mqtt-ws-bridge/rm/script/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/script/.keep rename to rm-mqtt-ws-bridge/rm/script/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb b/rm-mqtt-ws-bridge/rm/spec/factories/resources.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/resources.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/resources.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb b/rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/frbc/instruction.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/frbc/instruction.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/frbc/instruction.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb b/rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/handshake.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/handshake.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb b/rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/handshake_response.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/handshake_response.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/handshake_response.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb b/rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/reception_status.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/reception_status.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/reception_status.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb b/rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/select_control_type.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/s2/messages/select_control_type.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/s2/messages/select_control_type.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb b/rm-mqtt-ws-bridge/rm/spec/factories/uuid.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/factories/uuid.rb rename to rm-mqtt-ws-bridge/rm/spec/factories/uuid.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb b/rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/jobs/process_frbc_instruction_job_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb b/rm-mqtt-ws-bridge/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/jobs/publish_s2_ready_notification_job_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb b/rm-mqtt-ws-bridge/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/jobs/send_heartbeat_power_measurement_job_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb b/rm-mqtt-ws-bridge/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/jobs/send_heartbeat_power_measurements_job_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/base_factory_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/base_factory_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/base_factory_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/error_notifier_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/error_notifier_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/error_notifier_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/instruction_status_tracker_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/instruction_status_tracker_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/instruction_status_tracker_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/message_relayer_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/message_relayer_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/message_relayer_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_handler_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_logger_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/mqtt_message_logger_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_logger_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/connection_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/connection_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/connection_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_factory_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_factory_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_factory_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/dispatching_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/dispatching_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/dispatching_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/error_handling_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/error_handling_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/error_handling_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/replying_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler/replying_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler/replying_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_handler_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_handler_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/message_sender_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/message_sender_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/message_sender_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2/session_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2/session_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2/session_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_handler_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_logger_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/s2_message_logger_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/s2_message_logger_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/supervisor_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/supervisor_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/supervisor_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/traffic_log_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/lib/traffic_log_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/lib/traffic_log_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb b/rm-mqtt-ws-bridge/rm/spec/models/resource_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/models/resource_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/models/resource_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb b/rm-mqtt-ws-bridge/rm/spec/rails_helper.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/rails_helper.rb rename to rm-mqtt-ws-bridge/rm/spec/rails_helper.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb b/rm-mqtt-ws-bridge/rm/spec/requests/resources_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/requests/resources_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/requests/resources_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb b/rm-mqtt-ws-bridge/rm/spec/requests/traffic_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/requests/traffic_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/requests/traffic_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb b/rm-mqtt-ws-bridge/rm/spec/stekker_s2/application_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/stekker_s2/application_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/stekker_s2/application_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb b/rm-mqtt-ws-bridge/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/subscribers/log_mqtt_listener_subscriber_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb b/rm-mqtt-ws-bridge/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/subscribers/log_mqtt_message_handler_subscriber_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb b/rm-mqtt-ws-bridge/rm/spec/subscribers/log_session_subscriber_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/subscribers/log_session_subscriber_spec.rb rename to rm-mqtt-ws-bridge/rm/spec/subscribers/log_session_subscriber_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb b/rm-mqtt-ws-bridge/rm/spec/support/async.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/async.rb rename to rm-mqtt-ws-bridge/rm/spec/support/async.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb b/rm-mqtt-ws-bridge/rm/spec/support/factory_bot.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/factory_bot.rb rename to rm-mqtt-ws-bridge/rm/spec/support/factory_bot.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb b/rm-mqtt-ws-bridge/rm/spec/support/fake_mqtt_client.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_mqtt_client.rb rename to rm-mqtt-ws-bridge/rm/spec/support/fake_mqtt_client.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb b/rm-mqtt-ws-bridge/rm/spec/support/fake_websocket.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/fake_websocket.rb rename to rm-mqtt-ws-bridge/rm/spec/support/fake_websocket.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb b/rm-mqtt-ws-bridge/rm/spec/support/message_sender_stub.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/message_sender_stub.rb rename to rm-mqtt-ws-bridge/rm/spec/support/message_sender_stub.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb b/rm-mqtt-ws-bridge/rm/spec/support/shoulda_matchers.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/shoulda_matchers.rb rename to rm-mqtt-ws-bridge/rm/spec/support/shoulda_matchers.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb b/rm-mqtt-ws-bridge/rm/spec/support/websocket_server.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/spec/support/websocket_server.rb rename to rm-mqtt-ws-bridge/rm/spec/support/websocket_server.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/.keep b/rm-mqtt-ws-bridge/rm/vendor/.keep similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/.keep rename to rm-mqtt-ws-bridge/rm/vendor/.keep diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.gitignore similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.gitignore rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.gitignore diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.rspec similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.rspec rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.rspec diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.ruby-version similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/.ruby-version rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/.ruby-version diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Gemfile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Gemfile diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Gemfile.lock similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Gemfile.lock rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Gemfile.lock diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/LICENSE.txt similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/LICENSE.txt rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/LICENSE.txt diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/README.md similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/README.md rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/README.md diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Rakefile similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/Rakefile rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/Rakefile diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/console similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/console rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/console diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/generate_messages similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/generate_messages rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/generate_messages diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/setup similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/bin/setup rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/bin/setup diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/charge_status.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/charge_target.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/common.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/connect.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/disconnect.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/instruction_update.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_forecast.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_measurement.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/power_setpoint.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/json-schema/s2_ready_notification.schema.json diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/factories.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/fake_client.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/listener.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_factory.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/message_handler.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/messages.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/publisher.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/rspec/matchers.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/lib/stekker/mqtt/version.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/charge_status.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/charge_target.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/connect.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/connect.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/connect.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/disconnect.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/instruction_update.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_forecast.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_measurement.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/power_setpoint.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/factories/s2_ready_notification.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/spec_helper.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/spec_helper.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/spec_helper.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/listener_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_factory_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/message_handler_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/stekker/mqtt/publisher_spec.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/spec/support/factory_bot.rb diff --git a/resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec b/rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec similarity index 100% rename from resource-manager-mqtt-bridge-ruby/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec rename to rm-mqtt-ws-bridge/rm/vendor/stekker-mqtt/stekker-mqtt.gemspec From 9dbdb142a77ff8220d29324a3e242ba9e4b3ca76 Mon Sep 17 00:00:00 2001 From: Jorrit Nutma Date: Tue, 16 Jun 2026 15:52:05 +0200 Subject: [PATCH 07/10] Add two notes about the goal and scope of the reference implementation at beginning of README --- rm-mqtt-ws-bridge/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rm-mqtt-ws-bridge/README.md b/rm-mqtt-ws-bridge/README.md index b50aa95..d37b4b7 100644 --- a/rm-mqtt-ws-bridge/README.md +++ b/rm-mqtt-ws-bridge/README.md @@ -5,6 +5,10 @@ messages, stores the resource state, and translates each message into an [S2 pro message that it relays over a WebSocket to a **CEM** (Customer Energy Manager) peer. It also turns the CEM's `FRBC.Instruction` back into a Stekker `power_setpoint` on MQTT. +> Note: only FRBC messages are supported currently. + +> Note: This is by no means an attempt to standardize S2 message over MQTT. The MQTT communication is only supposed to be used internally to interface with your business logic. + The RM is the reference implementation itself. It lives in [`rm/`](rm/) and is the subject of most of this document. The last chapter, [The live three-panel demo](#the-live-three-panel-demo), shows the RM talking to both of its neighbors at once on a single screen, using two small demo stand-ins (a CEM and an MQTT From 10594b016f87ad7a24339e72b1fc3ff3c0441fdb Mon Sep 17 00:00:00 2001 From: Eric-Paul Lecluse <218289+epologee@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:55:37 +0200 Subject: [PATCH 08/10] Follow the official S2 message definitions --- rm-mqtt-ws-bridge/README.md | 6 +- rm-mqtt-ws-bridge/cem/server.rb | 1 - rm-mqtt-ws-bridge/rm/Gemfile | 2 +- rm-mqtt-ws-bridge/rm/Gemfile.lock | 4 +- .../app/jobs/process_frbc_instruction_job.rb | 6 +- .../rm/app/lib/mqtt_message_handler.rb | 1 - .../rm/app/lib/s2_message_handler.rb | 10 --- .../jobs/process_frbc_instruction_job_spec.rb | 8 +- .../rm/spec/lib/mqtt_message_handler_spec.rb | 1 - .../rm/spec/lib/s2_message_handler_spec.rb | 81 ------------------- 10 files changed, 12 insertions(+), 108 deletions(-) diff --git a/rm-mqtt-ws-bridge/README.md b/rm-mqtt-ws-bridge/README.md index d37b4b7..7f84c3e 100644 --- a/rm-mqtt-ws-bridge/README.md +++ b/rm-mqtt-ws-bridge/README.md @@ -225,7 +225,6 @@ Charge status → `FRBC.StorageStatus`: { "message_id": "", "message_type": "FRBC.StorageStatus", - "measurement_timestamp": "2026-06-04T10:05:00Z", "present_fill_level": 42.5 } ``` @@ -280,8 +279,7 @@ Inbound S2 `FRBC.Instruction` (representative fields): "message_type": "FRBC.Instruction", "operation_mode": "", "operation_mode_factor": 0.5, - "execution_time": "2024-01-01T12:00:00Z", - "duration": 600000 + "execution_time": "2024-01-01T12:00:00Z" } ``` @@ -294,7 +292,7 @@ Outbound Stekker `power_setpoint` (MQTT): "data": { "reference": "", "starts_at": "2024-01-01T12:00:00Z", - "ends_at": "2024-01-01T12:10:00Z", + "ends_at": "2024-01-02T00:00:00Z", "unit": "W", "value": 5520 } diff --git a/rm-mqtt-ws-bridge/cem/server.rb b/rm-mqtt-ws-bridge/cem/server.rb index e8c49ce..b64ff2f 100644 --- a/rm-mqtt-ws-bridge/cem/server.rb +++ b/rm-mqtt-ws-bridge/cem/server.rb @@ -81,7 +81,6 @@ def frbc_instruction_from(system_description) "actuator_id" => actuator.fetch("id"), "operation_mode" => mode.fetch("id"), "operation_mode_factor" => 0.5, - "duration" => 600_000, "execution_time" => (Time.now + 30).utc.iso8601, "abnormal_condition" => false, ) diff --git a/rm-mqtt-ws-bridge/rm/Gemfile b/rm-mqtt-ws-bridge/rm/Gemfile index ced0782..5243c3a 100644 --- a/rm-mqtt-ws-bridge/rm/Gemfile +++ b/rm-mqtt-ws-bridge/rm/Gemfile @@ -13,7 +13,7 @@ gem "pg", "~> 1.1" gem "propshaft" gem "puma", ">= 5.0" gem "rails", "~> 8.1.2" -gem "s2-ruby", github: "stekker/s2-ruby", ref: "18583316a64e1d27e4273fe64a73988562848a6d", require: "s2" +gem "s2-ruby", github: "stekker/s2-ruby", ref: "7c0e956a86ab4c81618253ca8efb57665aa453fa", require: "s2" gem "solid_cable" gem "solid_cache" gem "solid_queue" diff --git a/rm-mqtt-ws-bridge/rm/Gemfile.lock b/rm-mqtt-ws-bridge/rm/Gemfile.lock index f4cb144..23a5cc2 100644 --- a/rm-mqtt-ws-bridge/rm/Gemfile.lock +++ b/rm-mqtt-ws-bridge/rm/Gemfile.lock @@ -1,7 +1,7 @@ GIT remote: https://github.com/stekker/s2-ruby.git - revision: 18583316a64e1d27e4273fe64a73988562848a6d - ref: 18583316a64e1d27e4273fe64a73988562848a6d + revision: 7c0e956a86ab4c81618253ca8efb57665aa453fa + ref: 7c0e956a86ab4c81618253ca8efb57665aa453fa specs: s2-ruby (0.1.0) activesupport (>= 7.0) diff --git a/rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb b/rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb index e6ebb91..36753d3 100644 --- a/rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb +++ b/rm-mqtt-ws-bridge/rm/app/jobs/process_frbc_instruction_job.rb @@ -1,4 +1,6 @@ class ProcessFRBCInstructionJob < ApplicationJob + INSTRUCTION_DURATION = 12.hours.freeze + def perform(resource_id:, frbc_system_description_json:, frbc_instruction_json:) frbc_system_description = S2::Messages::FRBCSystemDescription.from_json!(frbc_system_description_json) frbc_instruction = S2::Messages::FRBCInstruction.from_json!(frbc_instruction_json) @@ -29,7 +31,7 @@ def mqtt_publisher def build_power_setpoint_message(resource_id, frbc_system_description, frbc_instruction) starts_at = Time.zone.parse(frbc_instruction.execution_time) - ends_at = starts_at + (frbc_instruction.duration / 1000.0).seconds if frbc_instruction.duration.present? + ends_at = starts_at + INSTRUCTION_DURATION Stekker::MQTT::Messages::PowerSetpoint.from_dynamic!( "message_type" => Stekker::MQTT::Messages::PowerSetpointMessageType::PowerSetpoint, @@ -37,7 +39,7 @@ def build_power_setpoint_message(resource_id, frbc_system_description, frbc_inst "data" => { "reference" => frbc_instruction.id, "starts_at" => starts_at.iso8601, - "ends_at" => ends_at&.iso8601, + "ends_at" => ends_at.iso8601, "unit" => "W", "value" => calc_power_setpoint(frbc_system_description, frbc_instruction), }, diff --git a/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb b/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb index a9366e1..d242194 100644 --- a/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb +++ b/rm-mqtt-ws-bridge/rm/app/lib/mqtt_message_handler.rb @@ -93,7 +93,6 @@ class MQTTMessageHandler < Stekker::MQTT::MessageHandler message: S2::Messages::FRBCStorageStatus.from_dynamic!( "message_id" => SecureRandom.uuid, "message_type" => S2::Messages::FRBCStorageStatusMessageType::FRBCStorageStatus, - "measurement_timestamp" => message.data.sampled_at, "present_fill_level" => message.data.value, ), ) diff --git a/rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb b/rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb index 3a9e45a..efe7fa9 100644 --- a/rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb +++ b/rm-mqtt-ws-bridge/rm/app/lib/s2_message_handler.rb @@ -63,7 +63,6 @@ class ProtocolVersionMismatch < S2::MessageHandler::BaseError; end assert_state! message, control_type: S2::Messages::ControlType::FillRateBasedControl assert_actuator_id! message assert_operation_mode_id! message - assert_duration! message reply_ok to: message @@ -97,13 +96,4 @@ def assert_operation_mode_id!(frbc_instruction) frbc_instruction.message_id, ) end - - def assert_duration!(frbc_instruction) - return if frbc_instruction.duration.present? && frbc_instruction.duration.positive? - - raise S2::MessageHandler::Asserting::InvalidContent.new( - "Invalid attribute: duration must be present and positive, got '#{frbc_instruction.duration.inspect}'", - frbc_instruction.message_id, - ) - end end diff --git a/rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb b/rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb index 1dadd92..9dcb7c4 100644 --- a/rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb +++ b/rm-mqtt-ws-bridge/rm/spec/jobs/process_frbc_instruction_job_spec.rb @@ -9,7 +9,6 @@ operation_mode: frbc_system_description.actuators.first.operation_modes.second.id, # Charging mode operation_mode_factor: 0.5, execution_time: "2024-01-01T12:00:00Z", - duration: 10.minutes.in_milliseconds, ) with_fake_mqtt_client do |mqtt_client| @@ -23,7 +22,7 @@ topic: "stekker/resources/#{resource.s2_identifier}/power_setpoints", message: { data: { - ends_at: "2024-01-01T12:10:00Z", + ends_at: "2024-01-02T00:00:00Z", reference: frbc_instruction.id, starts_at: "2024-01-01T12:00:00Z", unit: "W", @@ -52,7 +51,6 @@ operation_mode: frbc_system_description.actuators.first.operation_modes.second.id, # Charging mode operation_mode_factor: 0.5, execution_time: "2024-01-01T12:00:00Z", - duration: 10.minutes.in_milliseconds, ) with_fake_mqtt_client do |mqtt_client| @@ -68,7 +66,7 @@ data: { reference: frbc_instruction.id, starts_at: "2024-01-01T12:00:00Z", - ends_at: "2024-01-01T12:10:00Z", + ends_at: "2024-01-02T00:00:00Z", unit: "W", value: 7_590, }, @@ -102,7 +100,7 @@ topic: "stekker/resources/#{resource.s2_identifier}/power_setpoints", message: { data: { - ends_at: nil, + ends_at: (Time.zone.parse(frbc_instruction.execution_time) + 12.hours).iso8601, reference: frbc_instruction.id, starts_at: frbc_instruction.execution_time, unit: "W", diff --git a/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb index 48110bf..83c2629 100644 --- a/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb +++ b/rm-mqtt-ws-bridge/rm/spec/lib/mqtt_message_handler_spec.rb @@ -388,7 +388,6 @@ expect(message).to be_a(S2::Messages::FRBCStorageStatus) expect(message.attributes).to include( message_type: S2::Messages::FRBCStorageStatusMessageType::FRBCStorageStatus, - measurement_timestamp: valid_message.data.sampled_at, present_fill_level: valid_message.data.value, ) end diff --git a/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb b/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb index 27521b8..ce652f0 100644 --- a/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb +++ b/rm-mqtt-ws-bridge/rm/spec/lib/s2_message_handler_spec.rb @@ -344,7 +344,6 @@ :s2_frbc_instruction, actuator_id: frbc_system_description.actuators.first.id, operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, - duration: 5000, ) handler.handle_message(message.to_json) @@ -358,85 +357,6 @@ ) end - it "responds with invalid content error when the duration is zero" do - message_sender = MessageSenderStub.new - resource = create(:resource) - frbc_system_description = FRBCSystemDescriptionFactory.create(resource) - state = { - resource_id: resource.s2_identifier, - status: :initialized, - control_type: S2::Messages::ControlType::FillRateBasedControl, - frbc_system_description:, - } - handler = described_class.new(message_sender:, state:) - message = build( - :s2_frbc_instruction, - actuator_id: frbc_system_description.actuators.first.id, - operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, - duration: 0, - ) - - handler.handle_message(message.to_json) - - expect(message_sender.sent_messages.size).to eq(1) - expect(message_sender.sent_messages.first).to have_attributes( - message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, - status: S2::Messages::ReceptionStatusValues::InvalidContent, - diagnostic_label: "Invalid attribute: duration must be present and positive, got '0'", - subject_message_id: message.message_id, - ) - end - - it "responds with invalid content error when the duration is missing" do - message_sender = MessageSenderStub.new - resource = create(:resource) - frbc_system_description = FRBCSystemDescriptionFactory.create(resource) - state = { - resource_id: resource.s2_identifier, - status: :initialized, - control_type: S2::Messages::ControlType::FillRateBasedControl, - frbc_system_description:, - } - handler = described_class.new(message_sender:, state:) - message = build( - :s2_frbc_instruction, - actuator_id: frbc_system_description.actuators.first.id, - operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, - ) - - handler.handle_message(message.to_json) - - expect(message_sender.sent_messages.size).to eq(1) - expect(message_sender.sent_messages.first).to have_attributes( - message_type: S2::Messages::ReceptionStatusMessageType::ReceptionStatus, - status: S2::Messages::ReceptionStatusValues::InvalidContent, - diagnostic_label: "Invalid attribute: duration must be present and positive, got 'nil'", - subject_message_id: message.message_id, - ) - end - - it "does not schedule a job when the duration is invalid" do - message_sender = MessageSenderStub.new - resource = create(:resource) - frbc_system_description = FRBCSystemDescriptionFactory.create(resource) - state = { - resource_id: resource.s2_identifier, - status: :initialized, - control_type: S2::Messages::ControlType::FillRateBasedControl, - frbc_system_description:, - } - handler = described_class.new(message_sender:, state:) - message = build( - :s2_frbc_instruction, - actuator_id: frbc_system_description.actuators.first.id, - operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, - duration: 0, - ) - - expect { handler.handle_message(message.to_json) } - .not_to have_enqueued_job(ProcessFRBCInstructionJob) - end - it "schedules a ProcessFRBCInstructionJob" do message_sender = MessageSenderStub.new resource = create(:resource) @@ -452,7 +372,6 @@ :s2_frbc_instruction, actuator_id: frbc_system_description.actuators.first.id, operation_mode: frbc_system_description.actuators.first.operation_modes.first.id, - duration: 5000, ) expect { handler.handle_message(message.to_json) } From 61bedb741e575126f4b306ecb27b4ef4ddd29431 Mon Sep 17 00:00:00 2001 From: Jorrit Nutma Date: Mon, 22 Jun 2026 15:21:26 +0200 Subject: [PATCH 09/10] Add .gitignore for the node based mqtt panel --- rm-mqtt-ws-bridge/mqtt/.gitignore | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 rm-mqtt-ws-bridge/mqtt/.gitignore diff --git a/rm-mqtt-ws-bridge/mqtt/.gitignore b/rm-mqtt-ws-bridge/mqtt/.gitignore new file mode 100644 index 0000000..e5d70ce --- /dev/null +++ b/rm-mqtt-ws-bridge/mqtt/.gitignore @@ -0,0 +1,44 @@ +# Node modules +node_modules/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.log +logs/ + +# Runtime files +.pids/ +*.pid +tmp/ +temp/ + +# Environment +.env +.env.* + +# Coverage, testing and build outputs +.nyc_output/ +coverage/ +dist/ +build/ + +# Packages +*.tgz + +# Docker overrides and local compose files +docker-compose.override.yml +docker-compose.*.local.yml +/.docker/ + +# Editor directories and OS files +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp + +# Caches +.cache/ +.sass-cache/ From 734c6bfa58ab6144ae8dffdc9b490a2fbe08f467 Mon Sep 17 00:00:00 2001 From: Jorrit Nutma Date: Mon, 22 Jun 2026 15:22:44 +0200 Subject: [PATCH 10/10] Overwrite resouce_id in example messages with value from the environment var --- rm-mqtt-ws-bridge/mqtt/server.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rm-mqtt-ws-bridge/mqtt/server.js b/rm-mqtt-ws-bridge/mqtt/server.js index c5db202..594900c 100644 --- a/rm-mqtt-ws-bridge/mqtt/server.js +++ b/rm-mqtt-ws-bridge/mqtt/server.js @@ -26,6 +26,16 @@ const examplePayloadByType = {}; for (const file of readdirSync(examplesDir).filter((f) => f.endsWith(".json"))) { const type = file.replace(/\.json$/, ""); examplePayloadByType[type] = JSON.parse(readFileSync(join(examplesDir, file), "utf8")); + // ensure any resource_id in the example payloads matches configured RESOURCE_ID + const replaceResourceId = (val) => { + if (Array.isArray(val)) { + for (const item of val) replaceResourceId(item); + } else if (val && typeof val === "object") { + if (Object.prototype.hasOwnProperty.call(val, "resource_id")) val.resource_id = RESOURCE_ID; + for (const k of Object.keys(val)) replaceResourceId(val[k]); + } + }; + replaceResourceId(examplePayloadByType[type]); } function topicFor(suffix) {