diff --git a/lib/s2-ruby.rb b/lib/s2-ruby.rb index 42881a6..ec9361c 100644 --- a/lib/s2-ruby.rb +++ b/lib/s2-ruby.rb @@ -6,8 +6,20 @@ require_relative "s2/version" require_relative "s2/messages/types" -Dir[File.join(__dir__, "s2/schemas", "*.rb")].each { |file| require_relative file } -Dir[File.join(__dir__, "s2/messages", "*.rb")].each { |file| require_relative file } +require_relative "s2/schemas/number_range" + +require_relative "s2/messages/frbc_actuator_status" +require_relative "s2/messages/frbc_fill_level_target_profile" +require_relative "s2/messages/frbc_instruction" +require_relative "s2/messages/frbc_leakage_behaviour" +require_relative "s2/messages/frbc_storage_status" +require_relative "s2/messages/frbc_system_description" +require_relative "s2/messages/frbc_timer_status" +require_relative "s2/messages/frbc_usage_forecast" +require_relative "s2/messages/handshake" +require_relative "s2/messages/handshake_response" +require_relative "s2/messages/reception_status" +require_relative "s2/messages/resource_manager_details" require_relative "s2/message_factory" require_relative "s2/message_handler" diff --git a/lib/s2/messages/resource_manager_details.rb b/lib/s2/messages/resource_manager_details.rb new file mode 100644 index 0000000..2450712 --- /dev/null +++ b/lib/s2/messages/resource_manager_details.rb @@ -0,0 +1,309 @@ +module S2 + module Messages + module MessageType + ResourceManagerDetails = "ResourceManagerDetails" + end + + # POWER_ENVELOPE_BASED_CONTROL: Identifier for the Power Envelope Based Control type + # POWER_PROFILE_BASED_CONTROL: Identifier for the Power Profile Based Control type + # OPERATION_MODE_BASED_CONTROL: Identifier for the Operation Mode Based Control type + # FILL_RATE_BASED_CONTROL: Identifier for the Demand Driven Based Control type + # DEMAND_DRIVEN_BASED_CONTROL: Identifier for the Fill Rate Based Control type + # NOT_CONTROLABLE: Identifier that is to be used if no control is possible. Resources of + # this type can still provide measurements and forecast + # NO_SELECTION: Identifier that is to be used if no control type is or has been selected. + module ControlType + DemandDrivenBasedControl = "DEMAND_DRIVEN_BASED_CONTROL" + FillRateBasedControl = "FILL_RATE_BASED_CONTROL" + NoSelection = "NO_SELECTION" + NotControlable = "NOT_CONTROLABLE" + OperationModeBasedControl = "OPERATION_MODE_BASED_CONTROL" + PowerEnvelopeBasedControl = "POWER_ENVELOPE_BASED_CONTROL" + PowerProfileBasedControl = "POWER_PROFILE_BASED_CONTROL" + end + + # Currency to be used for all information regarding costs. Mandatory if cost information is + # published. + # + # Currency used when this resource gives cost information + module Currency + Aed = "AED" + Ang = "ANG" + Aud = "AUD" + Che = "CHE" + Chf = "CHF" + Chw = "CHW" + Eur = "EUR" + Gbp = "GBP" + Lbp = "LBP" + Lkr = "LKR" + Lrd = "LRD" + Lsl = "LSL" + Lyd = "LYD" + Mad = "MAD" + Mdl = "MDL" + Mga = "MGA" + Mkd = "MKD" + Mmk = "MMK" + Mnt = "MNT" + Mop = "MOP" + Mro = "MRO" + Mur = "MUR" + Mvr = "MVR" + Mwk = "MWK" + Mxn = "MXN" + Mxv = "MXV" + Myr = "MYR" + Mzn = "MZN" + NIO = "NIO" + Nad = "NAD" + Ngn = "NGN" + Nok = "NOK" + Npr = "NPR" + Nzd = "NZD" + OMR = "OMR" + PHP = "PHP" + Pab = "PAB" + Pen = "PEN" + Pgk = "PGK" + Pkr = "PKR" + Pln = "PLN" + Pyg = "PYG" + Qar = "QAR" + Ron = "RON" + Rsd = "RSD" + Rub = "RUB" + Rwf = "RWF" + SSP = "SSP" + Sar = "SAR" + Sbd = "SBD" + Scr = "SCR" + Sdg = "SDG" + Sek = "SEK" + Sgd = "SGD" + Shp = "SHP" + Sll = "SLL" + Sos = "SOS" + Srd = "SRD" + Std = "STD" + Syp = "SYP" + Szl = "SZL" + Thb = "THB" + Tjs = "TJS" + Tmt = "TMT" + Tnd = "TND" + Top = "TOP" + Try = "TRY" + Ttd = "TTD" + Twd = "TWD" + Tzs = "TZS" + Uah = "UAH" + Ugx = "UGX" + Usd = "USD" + Usn = "USN" + Uyi = "UYI" + Uyu = "UYU" + Uzs = "UZS" + Vef = "VEF" + Vnd = "VND" + Vuv = "VUV" + Wst = "WST" + XAG = "XAG" + Xau = "XAU" + Xba = "XBA" + Xbb = "XBB" + Xbc = "XBC" + Xbd = "XBD" + Xcd = "XCD" + Xof = "XOF" + Xpd = "XPD" + Xpf = "XPF" + Xpt = "XPT" + Xsu = "XSU" + Xts = "XTS" + Xua = "XUA" + Xxx = "XXX" + Yer = "YER" + Zar = "ZAR" + Zmw = "ZMW" + Zwl = "ZWL" + end + + + # ELECTRIC.POWER.L1: Electric power described in Watt on phase 1. If a device utilizes only + # one phase it should always use L1. + # ELECTRIC.POWER.L2: Electric power described in Watt on phase 2. Only applicable for 3 + # phase devices. + # ELECTRIC.POWER.L3: Electric power described in Watt on phase 3. Only applicable for 3 + # phase devices. + # ELECTRIC.POWER.3_PHASE_SYMMETRIC: Electric power described in Watt on when power is + # equally shared among the three phases. Only applicable for 3 phase devices. + # NATURAL_GAS.FLOW_RATE: Gas flow rate described in liters per second + # HYDROGEN.FLOW_RATE: Gas flow rate described in grams per second + # HEAT.TEMPERATURE: Heat described in degrees Celsius + # HEAT.FLOW_RATE: Flow rate of heat carrying gas or liquid in liters per second + # HEAT.THERMAL_POWER: Thermal power in Watt + # OIL.FLOW_RATE: Oil flow rate described in liters per hour + module CommodityQuantity + ElectricPower3_PhaseSymmetric = "ELECTRIC.POWER.3_PHASE_SYMMETRIC" + ElectricPowerL1 = "ELECTRIC.POWER.L1" + ElectricPowerL2 = "ELECTRIC.POWER.L2" + ElectricPowerL3 = "ELECTRIC.POWER.L3" + HeatFlowRate = "HEAT.FLOW_RATE" + HeatTemperature = "HEAT.TEMPERATURE" + HeatThermalPower = "HEAT.THERMAL_POWER" + HydrogenFlowRate = "HYDROGEN.FLOW_RATE" + NaturalGasFlowRate = "NATURAL_GAS.FLOW_RATE" + OilFlowRate = "OIL.FLOW_RATE" + end + + # Commodity the role refers to. + # + # GAS: Identifier for Commodity GAS + # HEAT: Identifier for Commodity HEAT + # ELECTRICITY: Identifier for Commodity ELECTRICITY + # OIL: Identifier for Commodity OIL + module Commodity + Electricity = "ELECTRICITY" + Gas = "GAS" + Heat = "HEAT" + Oil = "OIL" + end + + # Role type of the Resource Manager for the given commodity + # + # ENERGY_PRODUCER: Identifier for RoleType Producer + # ENERGY_CONSUMER: Identifier for RoleType Consumer + # ENERGY_STORAGE: Identifier for RoleType Storage + module RoleType + EnergyConsumer = "ENERGY_CONSUMER" + EnergyProducer = "ENERGY_PRODUCER" + EnergyStorage = "ENERGY_STORAGE" + end + + class Role < Dry::Struct + + # Commodity the role refers to. + attribute :commodity, Types::Commodity + + # Role type of the Resource Manager for the given commodity + attribute :role, Types::RoleType + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + commodity: d.fetch("commodity"), + role: d.fetch("role"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "commodity" => commodity, + "role" => role, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class ResourceManagerDetails < Dry::Struct + + # The control types supported by this Resource Manager. + attribute :available_control_types, Types.Array(Types::ControlType) + + # Currency to be used for all information regarding costs. Mandatory if cost information is + # published. + attribute :currency, Types::Currency.optional + + # Version identifier of the firmware used in the device (provided by the manufacturer) + attribute :firmware_version, Types::String.optional + + # The average time the combination of Resource Manager and HBES/BACS/SASS or (Smart) device + # needs to process and execute an instruction + attribute :instruction_processing_delay, Types::Integer + + # Name of Manufacturer + attribute :manufacturer, Types::String.optional + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Name of the model of the device (provided by the manufacturer) + attribute :model, Types::String.optional + + # Human readable name given by user + attribute :resource_manager_details_name, Types::String.optional + + # Indicates whether the ResourceManager is able to provide PowerForecasts + attribute :provides_forecast, Types::Bool + + # Array of all CommodityQuantities that this Resource Manager can provide measurements for. + attribute :provides_power_measurement_types, Types.Array(Types::CommodityQuantity) + + # Identifier of the Resource Manager. Must be unique within the scope of the CEM. + attribute :resource_id, Types::String + + # Each Resource Manager provides one or more energy Roles + attribute :roles, Types.Array(Role) + + # Serial number of the device (provided by the manufacturer) + attribute :serial_number, Types::String.optional + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + available_control_types: d.fetch("available_control_types"), + currency: d["currency"], + firmware_version: d["firmware_version"], + instruction_processing_delay: d.fetch("instruction_processing_delay"), + manufacturer: d["manufacturer"], + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + model: d["model"], + resource_manager_details_name: d["name"], + provides_forecast: d.fetch("provides_forecast"), + provides_power_measurement_types: d.fetch("provides_power_measurement_types"), + resource_id: d.fetch("resource_id"), + roles: d.fetch("roles").map { |x| Role.from_dynamic!(x) }, + serial_number: d["serial_number"], + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "available_control_types" => available_control_types, + "currency" => currency, + "firmware_version" => firmware_version, + "instruction_processing_delay" => instruction_processing_delay, + "manufacturer" => manufacturer, + "message_id" => message_id, + "message_type" => message_type, + "model" => model, + "name" => resource_manager_details_name, + "provides_forecast" => provides_forecast, + "provides_power_measurement_types" => provides_power_measurement_types, + "resource_id" => resource_id, + "roles" => roles.map { |x| x.to_dynamic }, + "serial_number" => serial_number, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/types.rb b/lib/s2/messages/types.rb index 80e660a..a12df92 100644 --- a/lib/s2/messages/types.rb +++ b/lib/s2/messages/types.rb @@ -10,24 +10,28 @@ module Types Double = Strict::Float | Strict::Integer MessageType = Strict::String.enum( - "Handshake", - "HandshakeResponse", - "ReceptionStatus", - "FRBC.StorageStatus", - "FRBC.Instruction", "FRBC.ActuatorStatus", - "FRBC.UsageForecast", "FRBC.FillLevelTargetProfile", + "FRBC.Instruction", "FRBC.LeakageBehaviour", + "FRBC.StorageStatus", "FRBC.SystemDescription", "FRBC.TimerStatus", "FRBC.UsageForecast", + "FRBC.UsageForecast", + "Handshake", + "HandshakeResponse", + "ReceptionStatus", + "ResourceManagerDetails" ) - CommodityQuantity = Strict::String.enum("ELECTRIC.POWER.3_PHASE_SYMMETRIC", "ELECTRIC.POWER.L1", "ELECTRIC.POWER.L2", "ELECTRIC.POWER.L3", "HEAT.FLOW_RATE", "HEAT.TEMPERATURE", "HEAT.THERMAL_POWER", "HYDROGEN.FLOW_RATE", "NATURAL_GAS.FLOW_RATE", "OIL.FLOW_RATE") Commodity = Strict::String.enum("ELECTRICITY", "GAS", "HEAT", "OIL") + CommodityQuantity = Strict::String.enum("ELECTRIC.POWER.3_PHASE_SYMMETRIC", "ELECTRIC.POWER.L1", "ELECTRIC.POWER.L2", "ELECTRIC.POWER.L3", "HEAT.FLOW_RATE", "HEAT.TEMPERATURE", "HEAT.THERMAL_POWER", "HYDROGEN.FLOW_RATE", "NATURAL_GAS.FLOW_RATE", "OIL.FLOW_RATE") + ControlType = Strict::String.enum("DEMAND_DRIVEN_BASED_CONTROL", "FILL_RATE_BASED_CONTROL", "NO_SELECTION", "NOT_CONTROLABLE", "OPERATION_MODE_BASED_CONTROL", "POWER_ENVELOPE_BASED_CONTROL", "POWER_PROFILE_BASED_CONTROL") + Currency = Strict::String.enum("AED", "ANG", "AUD", "CHE", "CHF", "CHW", "EUR", "GBP", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NIO", "NAD", "NGN", "NOK", "NPR", "NZD", "OMR", "PHP", "PAB", "PEN", "PGK", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SSP", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL") EnergyManagementRole = Strict::String.enum("CEM", "RM") ReceptionStatusValues = Strict::String.enum("INVALID_CONTENT", "INVALID_DATA", "INVALID_MESSAGE", "OK", "PERMANENT_ERROR", "TEMPORARY_ERROR") + RoleType = Strict::String.enum("ENERGY_CONSUMER", "ENERGY_PRODUCER", "ENERGY_STORAGE") end end end diff --git a/spec/lib/s2/message_factory_spec.rb b/spec/lib/s2/message_factory_spec.rb index 0fcc846..7f065ff 100644 --- a/spec/lib/s2/message_factory_spec.rb +++ b/spec/lib/s2/message_factory_spec.rb @@ -1,6 +1,6 @@ describe S2::MessageFactory do describe ".create" do - it "returns an instance of the correct message type" do + it "returns an instance of the Handshake message" do hash = { "message_id" => "123", "message_type" => "Handshake", @@ -10,6 +10,34 @@ expect(message).to be_a(S2::Messages::Handshake) end + it "returns an instance of the ResourceManagerDetails message" do + hash = JSON.parse <<~JSON + { + "message_type": "ResourceManagerDetails", + "message_id": "69b8ad18-9419-4e7f-bf04-eb5a0089e1e7", + "resource_id": "29f1ae55-e4f1-4a38-b4e7-db2feefdbd43", + "roles": [ + { + "role": "ENERGY_CONSUMER", + "commodity": "ELECTRICITY" + } + ], + "instruction_processing_delay": 1000, + "available_control_types": [ + "FILL_RATE_BASED_CONTROL" + ], + "currency": "EUR", + "provides_forecast": false, + "provides_power_measurement_types": [ + "ELECTRIC.POWER.L1" + ] + } + JSON + + message = described_class.create(hash) + expect(message).to be_a(S2::Messages::ResourceManagerDetails) + end + it "fails when no message type is given" do hash = { "message_id" => "123",