diff --git a/.gitignore b/.gitignore index 71aca8e..7c101ec 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /spec/reports/ /tmp/ spec/examples.txt +.idea diff --git a/.rubocop_disabled.yml b/.rubocop_disabled.yml index 8a1ff5b..877c180 100644 --- a/.rubocop_disabled.yml +++ b/.rubocop_disabled.yml @@ -14,3 +14,6 @@ Style/ClassAndModuleChildren: # Decide what we want Style/Documentation: # No, just no Enabled: false + +Naming/MethodParameterName: # single letter params are fine + Enabled: false diff --git a/README.md b/README.md index afd524e..e09a42b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # S2 -TODO: Delete this and the text below, and describe your gem +## Requirements -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/s2`. To experiment with that code, run `bin/console` for an interactive prompt. +```bash +brew install quicktype +``` + +## Code generation + +To generate boilerplate code for the S2 library, run the following command: +```bash +bin/gen-types +``` + +Move these to the `lib/s2` directory and adjust them according to the existing examples. + +```bash ## Installation diff --git a/lib/s2-ruby.rb b/lib/s2-ruby.rb index 1aab1c0..ec9361c 100644 --- a/lib/s2-ruby.rb +++ b/lib/s2-ruby.rb @@ -4,10 +4,23 @@ require "active_support/all" require_relative "s2/version" + require_relative "s2/messages/types" +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" require_relative "s2/message_handler_callbacks" diff --git a/lib/s2/messages/frbc_actuator_status.rb b/lib/s2/messages/frbc_actuator_status.rb new file mode 100644 index 0000000..7ae2731 --- /dev/null +++ b/lib/s2/messages/frbc_actuator_status.rb @@ -0,0 +1,68 @@ +module S2 + module Messages + module MessageType + FRBCActuatorStatus = "FRBC.ActuatorStatus" + end + + class FrbcActuatorStatus < Dry::Struct + + # ID of the FRBC.OperationMode that is presently active. + attribute :active_operation_mode_id, Types::String + + # ID of the actuator this messages refers to + attribute :actuator_id, Types::String + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # The number indicates the factor with which the FRBC.OperationMode is configured. The + # factor should be greater than or equal than 0 and less or equal to 1. + attribute :operation_mode_factor, Types::Double + + # ID of the FRBC.OperationMode that was active before the present one. This value shall + # always be provided, unless the active FRBC.OperationMode is the first FRBC.OperationMode + # the Resource Manager is aware of. + attribute :previous_operation_mode_id, Types::String.optional + + # Time at which the transition from the previous FRBC.OperationMode to the active + # FRBC.OperationMode was initiated. This value shall always be provided, unless the active + # FRBC.OperationMode is the first FRBC.OperationMode the Resource Manager is aware of. + attribute :transition_timestamp, Types::String.optional + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + active_operation_mode_id: d.fetch("active_operation_mode_id"), + actuator_id: d.fetch("actuator_id"), + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + operation_mode_factor: d.fetch("operation_mode_factor"), + previous_operation_mode_id: d["previous_operation_mode_id"], + transition_timestamp: d["transition_timestamp"], + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "active_operation_mode_id" => active_operation_mode_id, + "actuator_id" => actuator_id, + "message_id" => message_id, + "message_type" => message_type, + "operation_mode_factor" => operation_mode_factor, + "previous_operation_mode_id" => previous_operation_mode_id, + "transition_timestamp" => transition_timestamp, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_fill_level_target_profile.rb b/lib/s2/messages/frbc_fill_level_target_profile.rb new file mode 100644 index 0000000..cbca4ed --- /dev/null +++ b/lib/s2/messages/frbc_fill_level_target_profile.rb @@ -0,0 +1,122 @@ +module S2 + module Messages + module MessageType + FRBCFillLevelTargetProfile = "FRBC.FillLevelTargetProfile" + end + + # The target range in which the fill_level must be for the time period during which the + # element is active. The start of the range must be smaller or equal to the end of the + # range. The CEM must take best-effort actions to proactively achieve this target. + class NumberRange < Dry::Struct + + # Number that defines the end of the range + attribute :end_of_range, Types::Double + + # Number that defines the start of the range + attribute :start_of_range, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + end_of_range: d.fetch("end_of_range"), + start_of_range: d.fetch("start_of_range"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "end_of_range" => end_of_range, + "start_of_range" => start_of_range, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class FRBCFillLevelTargetProfileElement < Dry::Struct + + # The duration of the element. + attribute :duration, Types::Integer + + # The target range in which the fill_level must be for the time period during which the + # element is active. The start of the range must be smaller or equal to the end of the + # range. The CEM must take best-effort actions to proactively achieve this target. + attribute :fill_level_range, NumberRange + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + duration: d.fetch("duration"), + fill_level_range: NumberRange.from_dynamic!(d.fetch("fill_level_range")), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "duration" => duration, + "fill_level_range" => fill_level_range.to_dynamic, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module MessageType + FRBCFillLevelTargetProfile = "FRBC.FillLevelTargetProfile" + end + + class FrbcFillLevelTargetProfile < Dry::Struct + + # List of different fill levels that have to be targeted within a given duration. There + # shall be at least one element. Elements must be placed in chronological order. + attribute :elements, Types.Array(FRBCFillLevelTargetProfileElement) + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Time at which the FRBC.FillLevelTargetProfile starts. + attribute :start_time, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + elements: d.fetch("elements").map { |x| FRBCFillLevelTargetProfileElement.from_dynamic!(x) }, + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + start_time: d.fetch("start_time"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "elements" => elements.map { |x| x.to_dynamic }, + "message_id" => message_id, + "message_type" => message_type, + "start_time" => start_time, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_instruction.rb b/lib/s2/messages/frbc_instruction.rb new file mode 100644 index 0000000..932a148 --- /dev/null +++ b/lib/s2/messages/frbc_instruction.rb @@ -0,0 +1,71 @@ +module S2 + module Messages + module MessageType + FRBCInstruction = "FRBC.Instruction" + end + + class FrbcInstruction < Dry::Struct + + # Indicates if this is an instruction during an abnormal condition. + attribute :abnormal_condition, Types::Bool + + # ID of the actuator this instruction belongs to. + attribute :actuator_id, Types::String + + # Indicates the moment the execution of the instruction shall start. When the specified + # execution time is in the past, execution must start as soon as possible. + attribute :execution_time, Types::String + + # ID of the instruction. Must be unique in the scope of the Resource Manager, for at least + # the duration of the session between Resource Manager and CEM. + attribute :id, Types::String + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # ID of the FRBC.OperationMode that should be activated. + attribute :operation_mode, Types::String + + # The number indicates the factor with which the FRBC.OperationMode should be configured. + # The factor should be greater than or equal to 0 and less or equal to 1. + attribute :operation_mode_factor, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + abnormal_condition: d.fetch("abnormal_condition"), + actuator_id: d.fetch("actuator_id"), + execution_time: d.fetch("execution_time"), + id: d.fetch("id"), + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + operation_mode: d.fetch("operation_mode"), + operation_mode_factor: d.fetch("operation_mode_factor"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "abnormal_condition" => abnormal_condition, + "actuator_id" => actuator_id, + "execution_time" => execution_time, + "id" => id, + "message_id" => message_id, + "message_type" => message_type, + "operation_mode" => operation_mode, + "operation_mode_factor" => operation_mode_factor, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_leakage_behaviour.rb b/lib/s2/messages/frbc_leakage_behaviour.rb new file mode 100644 index 0000000..4cdbc90 --- /dev/null +++ b/lib/s2/messages/frbc_leakage_behaviour.rb @@ -0,0 +1,90 @@ +module S2 + module Messages + module MessageType + FRBCLeakageBehaviour = "FRBC.LeakageBehaviour" + end + + + class FRBCLeakageBehaviourElement < Dry::Struct + + # The fill level range for which this FRBC.LeakageBehaviourElement applies. The start of + # the range must be less than the end of the range. + attribute :fill_level_range, Schemas::NumberRange + + # Indicates how fast the momentary fill level will decrease per second due to leakage + # within the given range of the fill level. A positive value indicates that the fill level + # decreases over time due to leakage. + attribute :leakage_rate, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + fill_level_range: NumberRange.from_dynamic!(d.fetch("fill_level_range")), + leakage_rate: d.fetch("leakage_rate"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "fill_level_range" => fill_level_range.to_dynamic, + "leakage_rate" => leakage_rate, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module MessageType + FRBCLeakageBehaviour = "FRBC.LeakageBehaviour" + end + + class FrbcLeakageBehaviour < Dry::Struct + + # List of elements that model the leakage behaviour of the buffer. The fill_level_ranges of + # the elements must be contiguous. + attribute :elements, Types.Array(FRBCLeakageBehaviourElement) + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Moment this FRBC.LeakageBehaviour starts to be valid. If the FRBC.LeakageBehaviour is + # immediately valid, the DateTimeStamp should be now or in the past. + attribute :valid_from, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + elements: d.fetch("elements").map { |x| FRBCLeakageBehaviourElement.from_dynamic!(x) }, + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + valid_from: d.fetch("valid_from"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "elements" => elements.map { |x| x.to_dynamic }, + "message_id" => message_id, + "message_type" => message_type, + "valid_from" => valid_from, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_storage_status.rb b/lib/s2/messages/frbc_storage_status.rb new file mode 100644 index 0000000..82b70ee --- /dev/null +++ b/lib/s2/messages/frbc_storage_status.rb @@ -0,0 +1,43 @@ +module S2 + module Messages + module MessageType + FRBCStorageStatus = "FRBC.StorageStatus" + end + + class FrbcStorageStatus < Dry::Struct + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Present fill level of the Storage + attribute :present_fill_level, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + present_fill_level: d.fetch("present_fill_level"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "message_id" => message_id, + "message_type" => message_type, + "present_fill_level" => present_fill_level, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_system_description.rb b/lib/s2/messages/frbc_system_description.rb new file mode 100644 index 0000000..c715331 --- /dev/null +++ b/lib/s2/messages/frbc_system_description.rb @@ -0,0 +1,464 @@ +module S2 + module Messages + module MessageType + FRBCSystemDescription = "FRBC.SystemDescription" + end + + # The range of the fill level for which this FRBC.OperationModeElement applies. The start + # of the NumberRange shall be smaller than the end of the NumberRange. + # + # Indicates the change in fill_level per second. The lower_boundary of the NumberRange is + # associated with an operation_mode_factor of 0, the upper_boundary is associated with an + # operation_mode_factor of 1. + # + # Additional costs per second (e.g. wear, services) associated with this operation mode in + # the currency defined by the ResourceManagerDetails, excluding the commodity cost. The + # range is expressing uncertainty and is not linked to the operation_mode_factor. + # + # The range in which the fill_level should remain. It is expected of the CEM to keep the + # fill_level within this range. When the fill_level is not within this range, the Resource + # Manager can ignore instructions from the CEM (except during abnormal conditions). + + + # The power quantity the values refer to + # + # 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 + + class PowerRange < Dry::Struct + + # The power quantity the values refer to + attribute :commodity_quantity, Types::CommodityQuantity + + # Power value that defines the end of the range. + attribute :end_of_range, Types::Double + + # Power value that defines the start of the range. + attribute :start_of_range, Types::Double + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + commodity_quantity: d.fetch("commodity_quantity"), + end_of_range: d.fetch("end_of_range"), + start_of_range: d.fetch("start_of_range"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "commodity_quantity" => commodity_quantity, + "end_of_range" => end_of_range, + "start_of_range" => start_of_range, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class FRBCOperationModeElement < Dry::Struct + + # The range of the fill level for which this FRBC.OperationModeElement applies. The start + # of the NumberRange shall be smaller than the end of the NumberRange. + attribute :fill_level_range, S2::Schemas::NumberRange + + # Indicates the change in fill_level per second. The lower_boundary of the NumberRange is + # associated with an operation_mode_factor of 0, the upper_boundary is associated with an + # operation_mode_factor of 1. + attribute :fill_rate, S2::Schemas::NumberRange + + # The power produced or consumed by this operation mode. The start of each PowerRange is + # associated with an operation_mode_factor of 0, the end is associated with an + # operation_mode_factor of 1. In the array there must be at least one PowerRange, and at + # most one PowerRange per CommodityQuantity. + attribute :power_ranges, Types.Array(PowerRange) + + # Additional costs per second (e.g. wear, services) associated with this operation mode in + # the currency defined by the ResourceManagerDetails, excluding the commodity cost. The + # range is expressing uncertainty and is not linked to the operation_mode_factor. + attribute :running_costs, S2::Schemas::NumberRange.optional + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + fill_level_range: S2::Schemas::NumberRange.from_dynamic!(d.fetch("fill_level_range")), + fill_rate: S2::Schemas::NumberRange.from_dynamic!(d.fetch("fill_rate")), + power_ranges: d.fetch("power_ranges").map { |x| PowerRange.from_dynamic!(x) }, + running_costs: d["running_costs"] ? S2::Schemas::NumberRange.from_dynamic!(d["running_costs"]) : nil, + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "fill_level_range" => fill_level_range.to_dynamic, + "fill_rate" => fill_rate.to_dynamic, + "power_ranges" => power_ranges.map { |x| x.to_dynamic }, + "running_costs" => running_costs&.to_dynamic, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class FRBCOperationMode < Dry::Struct + + # Indicates if this FRBC.OperationMode may only be used during an abnormal condition + attribute :abnormal_condition_only, Types::Bool + + # Human readable name/description of the FRBC.OperationMode. This element is only intended + # for diagnostic purposes and not for HMI applications. + attribute :diagnostic_label, Types::String.optional + + # List of FRBC.OperationModeElements, which describe the properties of this + # FRBC.OperationMode depending on the fill_level. The fill_level_ranges of the items in the + # Array must be contiguous. + attribute :elements, Types.Array(FRBCOperationModeElement) + + # ID of the FRBC.OperationMode. Must be unique in the scope of the FRBC.ActuatorDescription + # in which it is used. + attribute :id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + abnormal_condition_only: d.fetch("abnormal_condition_only"), + diagnostic_label: d["diagnostic_label"], + elements: d.fetch("elements").map { |x| FRBCOperationModeElement.from_dynamic!(x) }, + id: d.fetch("id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "abnormal_condition_only" => abnormal_condition_only, + "diagnostic_label" => diagnostic_label, + "elements" => elements.map { |x| x.to_dynamic }, + "id" => id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # 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 + + class Timer < Dry::Struct + + # Human readable name/description of the Timer. This element is only intended for + # diagnostic purposes and not for HMI applications. + attribute :diagnostic_label, Types::String.optional + + # The time it takes for the Timer to finish after it has been started + attribute :duration, Types::Integer + + # ID of the Timer. Must be unique in the scope of the OMBC.SystemDescription, + # FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used. + attribute :id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + diagnostic_label: d["diagnostic_label"], + duration: d.fetch("duration"), + id: d.fetch("id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "diagnostic_label" => diagnostic_label, + "duration" => duration, + "id" => id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class Transition < Dry::Struct + + # Indicates if this Transition may only be used during an abnormal condition (see Clause ) + attribute :abnormal_condition_only, Types::Bool + + # List of IDs of Timers that block this Transition from initiating while at least one of + # these Timers is not yet finished + attribute :blocking_timers, Types.Array(Types::String) + + # ID of the OperationMode (exact type differs per ControlType) that should be switched from. + attribute :from, Types::String + + # ID of the Transition. Must be unique in the scope of the OMBC.SystemDescription, + # FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used. + attribute :id, Types::String + + # List of IDs of Timers that will be (re)started when this transition is initiated + attribute :start_timers, Types.Array(Types::String) + + # ID of the OperationMode (exact type differs per ControlType) that will be switched to. + attribute :to, Types::String + + # Absolute costs for going through this Transition in the currency as described in the + # ResourceManagerDetails. + attribute :transition_costs, Types::Double.optional + + # Indicates the time between the initiation of this Transition, and the time at which the + # device behaves according to the Operation Mode which is defined in the ‘to’ data element. + # When no value is provided it is assumed the transition duration is negligible. + attribute :transition_duration, Types::Integer.optional + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + abnormal_condition_only: d.fetch("abnormal_condition_only"), + blocking_timers: d.fetch("blocking_timers"), + from: d.fetch("from"), + id: d.fetch("id"), + start_timers: d.fetch("start_timers"), + to: d.fetch("to"), + transition_costs: d["transition_costs"], + transition_duration: d["transition_duration"], + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "abnormal_condition_only" => abnormal_condition_only, + "blocking_timers" => blocking_timers, + "from" => from, + "id" => id, + "start_timers" => start_timers, + "to" => to, + "transition_costs" => transition_costs, + "transition_duration" => transition_duration, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class FRBCActuatorDescription < Dry::Struct + + # Human readable name/description for the actuator. This element is only intended for + # diagnostic purposes and not for HMI applications. + attribute :diagnostic_label, Types::String.optional + + # ID of the Actuator. Must be unique in the scope of the Resource Manager, for at least the + # duration of the session between Resource Manager and CEM. + attribute :id, Types::String + + # Provided FRBC.OperationModes associated with this actuator + attribute :operation_modes, Types.Array(FRBCOperationMode) + + # List of all supported Commodities. + attribute :supported_commodities, Types.Array(Types::Commodity) + + # List of Timers associated with this actuator + attribute :timers, Types.Array(Timer) + + # Possible transitions between FRBC.OperationModes associated with this actuator. + attribute :transitions, Types.Array(Transition) + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + diagnostic_label: d["diagnostic_label"], + id: d.fetch("id"), + operation_modes: d.fetch("operation_modes").map { |x| FRBCOperationMode.from_dynamic!(x) }, + supported_commodities: d.fetch("supported_commodities"), + timers: d.fetch("timers").map { |x| Timer.from_dynamic!(x) }, + transitions: d.fetch("transitions").map { |x| Transition.from_dynamic!(x) }, + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "diagnostic_label" => diagnostic_label, + "id" => id, + "operation_modes" => operation_modes.map { |x| x.to_dynamic }, + "supported_commodities" => supported_commodities, + "timers" => timers.map { |x| x.to_dynamic }, + "transitions" => transitions.map { |x| x.to_dynamic }, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + # Details of the storage. + class FRBCStorageDescription < Dry::Struct + + # Human readable name/description of the storage (e.g. hot water buffer or battery). This + # element is only intended for diagnostic purposes and not for HMI applications. + attribute :diagnostic_label, Types::String.optional + + # Human readable description of the (physical) units associated with the fill_level (e.g. + # degrees Celsius or percentage state of charge). This element is only intended for + # diagnostic purposes and not for HMI applications. + attribute :fill_level_label, Types::String.optional + + # The range in which the fill_level should remain. It is expected of the CEM to keep the + # fill_level within this range. When the fill_level is not within this range, the Resource + # Manager can ignore instructions from the CEM (except during abnormal conditions). + attribute :fill_level_range, S2::Schemas::NumberRange + + # Indicates whether the Storage could provide a target profile for the fill level through + # the FRBC.FillLevelTargetProfile. + attribute :provides_fill_level_target_profile, Types::Bool + + # Indicates whether the Storage could provide details of power leakage behaviour through + # the FRBC.LeakageBehaviour. + attribute :provides_leakage_behaviour, Types::Bool + + # Indicates whether the Storage could provide a UsageForecast through the + # FRBC.UsageForecast. + attribute :provides_usage_forecast, Types::Bool + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + diagnostic_label: d["diagnostic_label"], + fill_level_label: d["fill_level_label"], + fill_level_range: NumberRange.from_dynamic!(d.fetch("fill_level_range")), + provides_fill_level_target_profile: d.fetch("provides_fill_level_target_profile"), + provides_leakage_behaviour: d.fetch("provides_leakage_behaviour"), + provides_usage_forecast: d.fetch("provides_usage_forecast"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "diagnostic_label" => diagnostic_label, + "fill_level_label" => fill_level_label, + "fill_level_range" => fill_level_range.to_dynamic, + "provides_fill_level_target_profile" => provides_fill_level_target_profile, + "provides_leakage_behaviour" => provides_leakage_behaviour, + "provides_usage_forecast" => provides_usage_forecast, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + class FrbcSystemDescription < Dry::Struct + + # Details of all Actuators. + attribute :actuators, Types.Array(FRBCActuatorDescription) + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Details of the storage. + attribute :storage, FRBCStorageDescription + + # Moment this FRBC.SystemDescription starts to be valid. If the system description is + # immediately valid, the DateTimeStamp should be now or in the past. + attribute :valid_from, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + actuators: d.fetch("actuators").map { |x| FRBCActuatorDescription.from_dynamic!(x) }, + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + storage: FRBCStorageDescription.from_dynamic!(d.fetch("storage")), + valid_from: d.fetch("valid_from"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "actuators" => actuators.map { |x| x.to_dynamic }, + "message_id" => message_id, + "message_type" => message_type, + "storage" => storage.to_dynamic, + "valid_from" => valid_from, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_timer_status.rb b/lib/s2/messages/frbc_timer_status.rb new file mode 100644 index 0000000..b1b5172 --- /dev/null +++ b/lib/s2/messages/frbc_timer_status.rb @@ -0,0 +1,55 @@ +module S2 + module Messages + module MessageType + FRBCTimerStatus = "FRBC.TimerStatus" + end + + class FrbcTimerStatus < Dry::Struct + + # The ID of the actuator the timer belongs to + attribute :actuator_id, Types::String + + # Indicates when the Timer will be finished. If the DateTimeStamp is in the future, the + # timer is not yet finished. If the DateTimeStamp is in the past, the timer is finished. If + # the timer was never started, the value can be an arbitrary DateTimeStamp in the past. + attribute :finished_at, Types::String + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # The ID of the timer this message refers to + attribute :timer_id, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + actuator_id: d.fetch("actuator_id"), + finished_at: d.fetch("finished_at"), + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + timer_id: d.fetch("timer_id"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "actuator_id" => actuator_id, + "finished_at" => finished_at, + "message_id" => message_id, + "message_type" => message_type, + "timer_id" => timer_id, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/frbc_usage_forecast.rb b/lib/s2/messages/frbc_usage_forecast.rb new file mode 100644 index 0000000..d2916a0 --- /dev/null +++ b/lib/s2/messages/frbc_usage_forecast.rb @@ -0,0 +1,123 @@ +module S2 + module Messages + module MessageType + FRBCUsageForecast = "FRBC.UsageForecast" + end + + class FRBCUsageForecastElement < Dry::Struct + + # Indicator for how long the given usage_rate is valid. + attribute :duration, Types::Integer + + # The most likely value for the usage rate; the expected increase or decrease of the + # fill_level per second. A positive value indicates that the fill level will decrease due + # to usage. + attribute :usage_rate_expected, Types::Double + + # The lower limit of the range with a 68 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_lower_68_ppr, Types::Double.optional + + # The lower limit of the range with a 95 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_lower_95_ppr, Types::Double.optional + + # The lower limit of the range with a 100 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_lower_limit, Types::Double.optional + + # The upper limit of the range with a 68 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_upper_68_ppr, Types::Double.optional + + # The upper limit of the range with a 95 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_upper_95_ppr, Types::Double.optional + + # The upper limit of the range with a 100 % probability that the usage rate is within that + # range. A positive value indicates that the fill level will decrease due to usage. + attribute :usage_rate_upper_limit, Types::Double.optional + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + duration: d.fetch("duration"), + usage_rate_expected: d.fetch("usage_rate_expected"), + usage_rate_lower_68_ppr: d["usage_rate_lower_68PPR"], + usage_rate_lower_95_ppr: d["usage_rate_lower_95PPR"], + usage_rate_lower_limit: d["usage_rate_lower_limit"], + usage_rate_upper_68_ppr: d["usage_rate_upper_68PPR"], + usage_rate_upper_95_ppr: d["usage_rate_upper_95PPR"], + usage_rate_upper_limit: d["usage_rate_upper_limit"], + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "duration" => duration, + "usage_rate_expected" => usage_rate_expected, + "usage_rate_lower_68PPR" => usage_rate_lower_68_ppr, + "usage_rate_lower_95PPR" => usage_rate_lower_95_ppr, + "usage_rate_lower_limit" => usage_rate_lower_limit, + "usage_rate_upper_68PPR" => usage_rate_upper_68_ppr, + "usage_rate_upper_95PPR" => usage_rate_upper_95_ppr, + "usage_rate_upper_limit" => usage_rate_upper_limit, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + + module MessageType + FRBCUsageForecast = "FRBC.UsageForecast" + end + + class FrbcUsageForecast < Dry::Struct + + # Further elements that model the profile. There shall be at least one element. Elements + # must be placed in chronological order. + attribute :elements, Types.Array(FRBCUsageForecastElement) + + # ID of this message + attribute :message_id, Types::String + + attribute :message_type, Types::MessageType + + # Time at which the FRBC.UsageForecast starts. + attribute :start_time, Types::String + + def self.from_dynamic!(d) + d = Types::Hash[d] + new( + elements: d.fetch("elements").map { |x| FRBCUsageForecastElement.from_dynamic!(x) }, + message_id: d.fetch("message_id"), + message_type: d.fetch("message_type"), + start_time: d.fetch("start_time"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "elements" => elements.map { |x| x.to_dynamic }, + "message_id" => message_id, + "message_type" => message_type, + "start_time" => start_time, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/lib/s2/messages/handshake.rb b/lib/s2/messages/handshake.rb index 9b0a348..83aa6c1 100644 --- a/lib/s2/messages/handshake.rb +++ b/lib/s2/messages/handshake.rb @@ -1,13 +1,3 @@ -# 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: -# -# handshake = Handshake.from_json! "{…}" -# puts handshake.supported_protocol_versions&.first -# -# If from_json! succeeds, the value returned matches the schema. - module S2 module Messages module MessageType diff --git a/lib/s2/messages/handshake_response.rb b/lib/s2/messages/handshake_response.rb index c1b239e..c035384 100644 --- a/lib/s2/messages/handshake_response.rb +++ b/lib/s2/messages/handshake_response.rb @@ -1,13 +1,3 @@ -# 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: -# -# handshake_response = HandshakeResponse.from_json! "{…}" -# puts handshake_response.message_id -# -# If from_json! succeeds, the value returned matches the schema. - module S2 module Messages module MessageType 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 3fd36ad..a12df92 100644 --- a/lib/s2/messages/types.rb +++ b/lib/s2/messages/types.rb @@ -3,17 +3,35 @@ module Messages module Types include Dry.Types(default: :nominal) - Hash = Strict::Hash - String = Strict::String + Integer = Strict::Integer + Bool = Strict::Bool + Hash = Strict::Hash + String = Strict::String + Double = Strict::Float | Strict::Integer - MessageType = Strict::String.enum( + MessageType = Strict::String.enum( + "FRBC.ActuatorStatus", + "FRBC.FillLevelTargetProfile", + "FRBC.Instruction", + "FRBC.LeakageBehaviour", + "FRBC.StorageStatus", + "FRBC.SystemDescription", + "FRBC.TimerStatus", + "FRBC.UsageForecast", + "FRBC.UsageForecast", "Handshake", "HandshakeResponse", "ReceptionStatus", + "ResourceManagerDetails" ) + 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/lib/s2/schemas/number_range.rb b/lib/s2/schemas/number_range.rb new file mode 100644 index 0000000..245a7ef --- /dev/null +++ b/lib/s2/schemas/number_range.rb @@ -0,0 +1,36 @@ +module S2 + module Schemas + # The fill level range for which this FRBC.LeakageBehaviourElement applies. The start of + # the range must be less than the end of the range. + class NumberRange < Dry::Struct + # Number that defines the end of the range + attribute :end_of_range, S2::Messages::Types::Double + + # Number that defines the start of the range + attribute :start_of_range, S2::Messages::Types::Double + + def self.from_dynamic!(d) + d = S2::Messages::Types::Hash[d] + new( + end_of_range: d.fetch("end_of_range"), + start_of_range: d.fetch("start_of_range"), + ) + end + + def self.from_json!(json) + from_dynamic!(JSON.parse(json)) + end + + def to_dynamic + { + "end_of_range" => end_of_range, + "start_of_range" => start_of_range, + } + end + + def to_json(options = nil) + JSON.generate(to_dynamic, options) + end + end + end +end diff --git a/spec/lib/s2/message_factory_spec.rb b/spec/lib/s2/message_factory_spec.rb index 0fcc846..2a2bb1c 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",