novacloud_client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop_todo.yml +5 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +142 -0
  6. data/Rakefile +12 -0
  7. data/lib/novacloud_client/client.rb +110 -0
  8. data/lib/novacloud_client/configuration.rb +31 -0
  9. data/lib/novacloud_client/errors.rb +26 -0
  10. data/lib/novacloud_client/middleware/authentication.rb +49 -0
  11. data/lib/novacloud_client/middleware/error_handler.rb +62 -0
  12. data/lib/novacloud_client/objects/base.rb +49 -0
  13. data/lib/novacloud_client/objects/control_log_entry.rb +21 -0
  14. data/lib/novacloud_client/objects/control_result.rb +55 -0
  15. data/lib/novacloud_client/objects/player.rb +37 -0
  16. data/lib/novacloud_client/objects/player_status.rb +29 -0
  17. data/lib/novacloud_client/objects/queued_request.rb +17 -0
  18. data/lib/novacloud_client/objects/screen.rb +21 -0
  19. data/lib/novacloud_client/objects/screen_detail.rb +29 -0
  20. data/lib/novacloud_client/objects/screen_monitor.rb +12 -0
  21. data/lib/novacloud_client/objects/solutions/offline_export_result.rb +80 -0
  22. data/lib/novacloud_client/objects/solutions/over_spec_detection_result.rb +80 -0
  23. data/lib/novacloud_client/objects/solutions/publish_result.rb +32 -0
  24. data/lib/novacloud_client/resources/base.rb +29 -0
  25. data/lib/novacloud_client/resources/concerns/payload_serializer.rb +46 -0
  26. data/lib/novacloud_client/resources/control.rb +250 -0
  27. data/lib/novacloud_client/resources/logs.rb +39 -0
  28. data/lib/novacloud_client/resources/players.rb +120 -0
  29. data/lib/novacloud_client/resources/scheduled_control.rb +221 -0
  30. data/lib/novacloud_client/resources/screens.rb +66 -0
  31. data/lib/novacloud_client/resources/solutions.rb +154 -0
  32. data/lib/novacloud_client/support/key_transform.rb +32 -0
  33. data/lib/novacloud_client/version.rb +5 -0
  34. data/lib/novacloud_client.rb +9 -0
  35. data/sig/novacloud_client.rbs +4 -0
  36. metadata +97 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ # Represents a screen/device entry from VNNOXCare APIs.
8
+ class Screen < Base
9
+ attr_accessor :sid, :name, :mac, :sn, :address, :longitude, :latitude,
10
+ :status, :camera, :brightness, :env_brightness
11
+
12
+ def online?
13
+ status.to_i == 1
14
+ end
15
+
16
+ def camera_enabled?
17
+ camera.to_i == 1
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ # Represents detailed telemetry for a screen, including nested hardware info.
8
+ class ScreenDetail < Base
9
+ attr_accessor :identifier, :input_source, :mac, :master_control, :module,
10
+ :monitor_card, :receiving_card, :screen, :sid, :smart_module, :sn
11
+
12
+ NESTED_HASH_FIELDS = %i[input_source master_control module monitor_card receiving_card screen smart_module].freeze
13
+
14
+ def initialize(attributes = {})
15
+ super
16
+ ensure_nested_hashes!
17
+ end
18
+
19
+ private
20
+
21
+ def ensure_nested_hashes!
22
+ NESTED_HASH_FIELDS.each do |field|
23
+ value = public_send(field)
24
+ public_send("#{field}=", value || {})
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ # Represents high-level monitoring metrics for a screen device.
8
+ class ScreenMonitor < Base
9
+ attr_accessor :display_device, :brightness, :env_brightness, :height, :width, :sn
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ module Solutions
8
+ # Represents the payload returned from the offline export endpoint.
9
+ class OfflineExportResult < Base
10
+ attr_reader :display_solutions, :play_relations, :play_solutions,
11
+ :playlists, :schedule_constraints, :plan_json
12
+
13
+ def display_solutions=(value)
14
+ @display_solutions = build_artifact(value)
15
+ end
16
+
17
+ def play_relations=(value)
18
+ @play_relations = build_artifact(value)
19
+ end
20
+
21
+ def play_solutions=(value)
22
+ @play_solutions = build_artifact(value)
23
+ end
24
+
25
+ def playlists=(value)
26
+ @playlists = build_artifact(value)
27
+ end
28
+
29
+ def schedule_constraints=(value)
30
+ @schedule_constraints = build_artifact(value)
31
+ end
32
+
33
+ def plan_json=(value)
34
+ @plan_json = build_artifact(value)
35
+ end
36
+
37
+ private
38
+
39
+ def build_artifact(value)
40
+ return nil if value.nil?
41
+
42
+ if value.is_a?(Array)
43
+ value.map { |artifact| Artifact.new(artifact) }
44
+ else
45
+ Artifact.new(value)
46
+ end
47
+ end
48
+
49
+ # Represents a downloadable artifact (JSON, playlist, etc.) returned by the export.
50
+ class Artifact < Base
51
+ attr_accessor :md5, :file_name, :url, :program_name
52
+
53
+ attr_writer :support_md5_checkout
54
+
55
+ def support_md5_checkout
56
+ return nil if @support_md5_checkout.nil?
57
+
58
+ !!@support_md5_checkout
59
+ end
60
+
61
+ def support_md5_checkout?
62
+ support_md5_checkout
63
+ end
64
+
65
+ # Legacy camelCase accessors for backward compatibility with existing clients.
66
+ # Legacy camelCase accessors for backward compatibility with existing clients.
67
+ # rubocop:disable Naming/PredicatePrefix
68
+ def is_support_md5_checkout?
69
+ support_md5_checkout?
70
+ end
71
+
72
+ def is_support_md5_checkout=(value)
73
+ self.support_md5_checkout = value
74
+ end
75
+ # rubocop:enable Naming/PredicatePrefix
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ module Solutions
8
+ # Represents the payload returned when checking program over-specification.
9
+ class OverSpecDetectionResult < Base
10
+ attr_accessor :logid, :status
11
+ attr_reader :items
12
+
13
+ def initialize(attributes = {})
14
+ @items = []
15
+ super
16
+ end
17
+
18
+ def data=(value)
19
+ @items = Array(value).map { |entry| Item.new(entry) }
20
+ end
21
+
22
+ alias data items
23
+
24
+ # Entry describing over-specification findings for a specific set of players.
25
+ class Item < Base
26
+ attr_accessor :over_spec_type
27
+ attr_reader :over_spec, :player_ids
28
+
29
+ def initialize(attributes = {})
30
+ @details = []
31
+ super
32
+ end
33
+
34
+ def over_spec=(value)
35
+ @over_spec = !!value
36
+ end
37
+
38
+ def over_spec?
39
+ over_spec
40
+ end
41
+
42
+ def player_ids=(value)
43
+ @player_ids = Array(value)
44
+ end
45
+
46
+ def over_spec_detail=(value)
47
+ @details = Array(value).map { |detail| Detail.new(detail) }
48
+ end
49
+
50
+ def details
51
+ @details ||= []
52
+ end
53
+
54
+ # Detail record for a widget that exceeds specifications.
55
+ class Detail < Base
56
+ attr_accessor :page_id, :widget_id
57
+ attr_reader :over_spec_error_code, :recommendation
58
+
59
+ def over_spec_error_code=(value)
60
+ @over_spec_error_code = Array(value)
61
+ end
62
+
63
+ def over_spec_error_codes
64
+ @over_spec_error_code
65
+ end
66
+
67
+ def recommend=(value)
68
+ @recommendation = value ? Recommendation.new(value) : nil
69
+ end
70
+
71
+ # Suggested adjustments for bringing a widget within spec.
72
+ class Recommendation < Base
73
+ attr_accessor :width, :height, :postfix, :fps, :byte_rate, :codec
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module NovacloudClient
6
+ module Objects
7
+ module Solutions
8
+ # Represents the publish result structure returned by solution endpoints.
9
+ class PublishResult < Base
10
+ def success=(value)
11
+ @successful = Array(value).compact
12
+ end
13
+
14
+ def fail=(value)
15
+ @failed = Array(value).compact
16
+ end
17
+
18
+ def all_successful?
19
+ failed.empty?
20
+ end
21
+
22
+ def failed
23
+ @failed ||= []
24
+ end
25
+
26
+ def successful
27
+ @successful ||= []
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NovacloudClient
4
+ module Resources
5
+ # Shared helpers for resource classes built on top of the client.
6
+ class Base
7
+ attr_reader :client
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ private
14
+
15
+ def get(endpoint, params: {})
16
+ client.request(http_method: :get, endpoint: endpoint, params: params)
17
+ end
18
+
19
+ def post(endpoint, params: {})
20
+ client.request(http_method: :post, endpoint: endpoint, params: params)
21
+ end
22
+
23
+ def validate_player_ids!(ids, max: 100)
24
+ raise ArgumentError, "player_ids cannot be empty" if ids.nil? || ids.empty?
25
+ raise ArgumentError, "maximum #{max} player IDs allowed" if ids.size > max
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NovacloudClient
4
+ module Resources
5
+ module Concerns
6
+ # Provides camelCase serialization helpers for API payloads.
7
+ module PayloadSerializer
8
+ private
9
+
10
+ def serialize_component(component)
11
+ case component
12
+ when nil
13
+ nil
14
+ when Hash
15
+ component.each_with_object({}) do |(key, value), result|
16
+ result[camelize_key(key)] = serialize_component(value)
17
+ end
18
+ when Array
19
+ component.map { |item| serialize_component(item) }
20
+ else
21
+ component.respond_to?(:to_h) ? serialize_component(component.to_h) : component
22
+ end
23
+ end
24
+
25
+ def camelize_key(key)
26
+ return key unless key.is_a?(String) || key.is_a?(Symbol)
27
+
28
+ string = key.to_s
29
+ normalized = string
30
+ .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
31
+ .tr("-", "_")
32
+ .downcase
33
+
34
+ parts = normalized.split("_")
35
+ camelized = parts.map.with_index do |segment, index|
36
+ next segment if index.zero?
37
+
38
+ segment.capitalize
39
+ end
40
+
41
+ camelized.join
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/control_result"
5
+ require_relative "../objects/queued_request"
6
+
7
+ module NovacloudClient
8
+ module Resources
9
+ # Resource wrapper for control commands.
10
+ #
11
+ # @example Set player brightness
12
+ # client.control.brightness(
13
+ # player_ids: ["player-1"],
14
+ # brightness: 80,
15
+ # notice_url: "https://example.com/callback"
16
+ # )
17
+ #
18
+ # @example Check a queued request
19
+ # request = client.control.reboot(player_ids: ["player-1"], notice_url: callback)
20
+ # client.control.request_result(request_id: request.request_id)
21
+ class Control < Base
22
+ MAX_BATCH = 100
23
+
24
+ # Adjust brightness asynchronously for a batch of players.
25
+ #
26
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
27
+ # @param brightness [Integer] percentage value between 0 and 100
28
+ # @param notice_url [String] HTTPS callback endpoint for async results
29
+ # @return [NovacloudClient::Objects::QueuedRequest]
30
+ # @raise [ArgumentError] when validation fails
31
+ def brightness(player_ids:, brightness:, notice_url:)
32
+ validate_percentage!(brightness, "brightness")
33
+ enqueue_control(
34
+ endpoint: "/v2/player/real-time-control/brightness",
35
+ player_ids: player_ids,
36
+ notice_url: notice_url,
37
+ extra_payload: { brightness: brightness }
38
+ )
39
+ end
40
+
41
+ # Adjust volume asynchronously for a batch of players.
42
+ #
43
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
44
+ # @param volume [Integer] percentage value between 0 and 100
45
+ # @param notice_url [String] HTTPS callback endpoint for async results
46
+ # @return [NovacloudClient::Objects::QueuedRequest]
47
+ # @raise [ArgumentError] when validation fails
48
+ def volume(player_ids:, volume:, notice_url:)
49
+ validate_percentage!(volume, "volume")
50
+ enqueue_control(
51
+ endpoint: "/v2/player/real-time-control/volume",
52
+ player_ids: player_ids,
53
+ notice_url: notice_url,
54
+ extra_payload: { volume: volume }
55
+ )
56
+ end
57
+
58
+ # Switch the video input source (e.g., HDMI1) for the selected players.
59
+ #
60
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
61
+ # @param source [String] identifier for the input source defined by NovaCloud (e.g., "HDMI1")
62
+ # @param notice_url [String] HTTPS callback endpoint for async results
63
+ # @return [NovacloudClient::Objects::QueuedRequest]
64
+ # @raise [ArgumentError] when validation fails
65
+ def video_source(player_ids:, source:, notice_url:)
66
+ validate_presence!(source, "source")
67
+ enqueue_control(
68
+ endpoint: "/v2/player/real-time-control/video-source",
69
+ player_ids: player_ids,
70
+ notice_url: notice_url,
71
+ extra_payload: { videoSource: source }
72
+ )
73
+ end
74
+
75
+ # Toggle screen power state for the selected players.
76
+ #
77
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
78
+ # @param state [Symbol, String, Integer, TrueClass, FalseClass] desired power state
79
+ # (:on, :off, true, false, 1, or 0)
80
+ # @param notice_url [String] HTTPS callback endpoint for async results
81
+ # @return [NovacloudClient::Objects::QueuedRequest]
82
+ # @raise [ArgumentError] when validation fails
83
+ def screen_power(player_ids:, state:, notice_url:)
84
+ payload = { option: normalize_power_state(state) }
85
+ enqueue_control(
86
+ endpoint: "/v2/player/real-time-control/power",
87
+ player_ids: player_ids,
88
+ notice_url: notice_url,
89
+ extra_payload: payload
90
+ )
91
+ end
92
+
93
+ # Request a black screen/normal screen toggle for the selected players.
94
+ #
95
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
96
+ # @param status [String, Symbol] desired screen mode (:open, :close, "OPEN", "CLOSE")
97
+ # @param notice_url [String] HTTPS callback endpoint for async results
98
+ # @return [NovacloudClient::Objects::QueuedRequest]
99
+ # @raise [ArgumentError] when validation fails
100
+ def screen_status(player_ids:, status:, notice_url:)
101
+ payload = { status: normalize_screen_status(status) }
102
+ enqueue_control(
103
+ endpoint: "/v2/player/real-time-control/screen-status",
104
+ player_ids: player_ids,
105
+ notice_url: notice_url,
106
+ extra_payload: payload
107
+ )
108
+ end
109
+
110
+ # Trigger screenshots for the selected players.
111
+ #
112
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
113
+ # @param notice_url [String] HTTPS callback endpoint receiving screenshot info
114
+ # @return [NovacloudClient::Objects::QueuedRequest]
115
+ # @raise [ArgumentError] when validation fails
116
+ def screenshot(player_ids:, notice_url:)
117
+ enqueue_control(
118
+ endpoint: "/v2/player/real-time-control/screen-capture",
119
+ player_ids: player_ids,
120
+ notice_url: notice_url
121
+ )
122
+ end
123
+
124
+ # Reboot one or more players asynchronously.
125
+ #
126
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
127
+ # @param notice_url [String] HTTPS callback endpoint for async results
128
+ # @return [NovacloudClient::Objects::QueuedRequest]
129
+ # @raise [ArgumentError] when validation fails
130
+ def reboot(player_ids:, notice_url:)
131
+ enqueue_control(
132
+ endpoint: "/v2/player/real-time-control/reboot",
133
+ player_ids: player_ids,
134
+ notice_url: notice_url
135
+ )
136
+ end
137
+
138
+ # Configure NTP time synchronization for the selected players.
139
+ #
140
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
141
+ # @param server [String] NTP server hostname
142
+ # @param enable [Boolean] flag indicating whether to enable synchronization
143
+ # @return [NovacloudClient::Objects::ControlResult]
144
+ def ntp_sync(player_ids:, server:, enable:)
145
+ validate_player_ids!(player_ids, max: MAX_BATCH)
146
+ validate_presence!(server, "server")
147
+ validate_boolean!(enable, "enable")
148
+
149
+ payload = {
150
+ playerIds: player_ids,
151
+ server: server,
152
+ enable: !enable.nil?
153
+ }
154
+
155
+ response = post("/v2/player/real-time-control/ntp", params: payload)
156
+ Objects::ControlResult.new(response)
157
+ end
158
+
159
+ # Toggle synchronous playback mode in real-time.
160
+ #
161
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
162
+ # @param option [Integer, Symbol, String, TrueClass, FalseClass] desired synchronous state
163
+ # @return [NovacloudClient::Objects::ControlResult]
164
+ def synchronous_playback(player_ids:, option:)
165
+ validate_player_ids!(player_ids, max: MAX_BATCH)
166
+
167
+ payload = {
168
+ playerIds: player_ids,
169
+ option: normalize_sync_option(option)
170
+ }
171
+
172
+ response = post("/v2/player/real-time-control/simulcast", params: payload)
173
+ Objects::ControlResult.new(response)
174
+ end
175
+
176
+ # Fetch the aggregated result of a previously queued control request.
177
+ #
178
+ # @param request_id [String] identifier returned by the queueing endpoints
179
+ # @return [NovacloudClient::Objects::ControlResult]
180
+ # @raise [ArgumentError] when the request ID is blank
181
+ def request_result(request_id:)
182
+ raise ArgumentError, "request_id is required" if request_id.to_s.strip.empty?
183
+
184
+ response = get("/v2/player/control/request-result", params: { requestId: request_id })
185
+ Objects::ControlResult.new(response)
186
+ end
187
+
188
+ private
189
+
190
+ def validate_percentage!(value, field)
191
+ return if value.is_a?(Integer) && value.between?(0, 100)
192
+
193
+ raise ArgumentError, "#{field} must be an integer between 0 and 100"
194
+ end
195
+
196
+ def validate_notice_url!(notice_url)
197
+ raise ArgumentError, "notice_url is required" if notice_url.to_s.strip.empty?
198
+ end
199
+
200
+ def validate_presence!(value, field)
201
+ raise ArgumentError, "#{field} is required" if value.to_s.strip.empty?
202
+ end
203
+
204
+ def validate_boolean!(value, field)
205
+ return if [true, false].include?(value)
206
+
207
+ raise ArgumentError, "#{field} must be true or false"
208
+ end
209
+
210
+ def enqueue_control(endpoint:, player_ids:, notice_url:, extra_payload: {})
211
+ validate_player_ids!(player_ids, max: MAX_BATCH)
212
+ validate_notice_url!(notice_url)
213
+
214
+ payload = { playerIds: player_ids }
215
+ payload.merge!(extra_payload)
216
+ payload[:noticeUrl] = notice_url
217
+
218
+ response = post(endpoint, params: payload)
219
+ Objects::QueuedRequest.new(response)
220
+ end
221
+
222
+ def normalize_power_state(state)
223
+ case state
224
+ when true, :on, "on", "ON", 1 then 1
225
+ when false, :off, "off", "OFF", 0 then 0
226
+ else
227
+ raise ArgumentError, "state must be one of :on, :off, true, false, or 0/1"
228
+ end
229
+ end
230
+
231
+ def normalize_screen_status(status)
232
+ case status
233
+ when :open, "open", "OPEN" then "OPEN"
234
+ when :close, "close", "CLOSE", :closed then "CLOSE"
235
+ else
236
+ raise ArgumentError, "status must be OPEN or CLOSE"
237
+ end
238
+ end
239
+
240
+ def normalize_sync_option(option)
241
+ case option
242
+ when true, :on, "on", "ON", 1 then 1
243
+ when false, :off, "off", "OFF", 0 then 0
244
+ else
245
+ raise ArgumentError, "option must be one of :on, :off, true, false, or 0/1"
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/control_log_entry"
5
+
6
+ module NovacloudClient
7
+ module Resources
8
+ # Resource wrapper for log-related endpoints.
9
+ class Logs < Base
10
+ # Retrieve remote control execution history for a player.
11
+ #
12
+ # @param player_id [String] NovaCloud player identifier
13
+ # @param start [Integer] pagination offset (defaults to 0)
14
+ # @param count [Integer] number of records to request (defaults to 20)
15
+ # @param task_type [Integer, nil] optional numeric filter defined by NovaCloud
16
+ # @return [Array<NovacloudClient::Objects::ControlLogEntry>]
17
+ def control_history(player_id:, start: 0, count: 20, task_type: nil)
18
+ validate_presence!(player_id, "player_id")
19
+
20
+ params = {
21
+ "playerId" => player_id,
22
+ "start" => start.to_s,
23
+ "count" => count.to_s
24
+ }
25
+ params["taskType"] = task_type.to_s if task_type
26
+
27
+ response = get("/v2/logs/remote-control", params: params)
28
+ rows = response.fetch("rows", [])
29
+ rows.map { |attrs| Objects::ControlLogEntry.new(attrs) }
30
+ end
31
+
32
+ private
33
+
34
+ def validate_presence!(value, field)
35
+ raise ArgumentError, "#{field} is required" if value.to_s.strip.empty?
36
+ end
37
+ end
38
+ end
39
+ end