alula-ruby 2.15.1 → 2.16.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION.md +1 -0
  3. data/bin/console +1 -0
  4. data/lib/alula/api_resource.rb +17 -20
  5. data/lib/alula/dcp/resource_attributes.rb +128 -7
  6. data/lib/alula/dcp/resource_state.rb +152 -0
  7. data/lib/alula/dcp/staged_value_methods.rb +50 -0
  8. data/lib/alula/dcp_resource.rb +119 -70
  9. data/lib/alula/monkey_patches.rb +27 -1
  10. data/lib/alula/resource_attributes.rb +12 -5
  11. data/lib/alula/resources/config_template.rb +49 -0
  12. data/lib/alula/resources/dcp/config/bus_modules/bus_module.rb +39 -0
  13. data/lib/alula/resources/dcp/config/bus_modules/dws_bus_module.rb +12 -0
  14. data/lib/alula/resources/dcp/config/bus_modules/input_bus_module.rb +12 -0
  15. data/lib/alula/resources/dcp/config/bus_modules/ism_transceiver_bus_module.rb +18 -0
  16. data/lib/alula/resources/dcp/config/bus_modules/output_bus_module.rb +12 -0
  17. data/lib/alula/resources/dcp/config/bus_modules/touchpad_bus_module.rb +17 -0
  18. data/lib/alula/resources/dcp/config/bus_modules/zwave_bus_module.rb +12 -0
  19. data/lib/alula/resources/dcp/config/bus_modules.rb +21 -0
  20. data/lib/alula/resources/dcp/config/comm_data/auto_comm_test_interval.rb +24 -0
  21. data/lib/alula/resources/dcp/config/comm_data/server_heartbeats.rb +25 -0
  22. data/lib/alula/resources/dcp/config/comm_data/server_keep_alives.rb +25 -0
  23. data/lib/alula/resources/dcp/config/comm_data.rb +27 -0
  24. data/lib/alula/resources/dcp/config/panel_options/ac_fail_detect_delay.rb +23 -0
  25. data/lib/alula/resources/dcp/config/panel_options/event_reporting_delay.rb +24 -0
  26. data/lib/alula/resources/dcp/config/panel_options/key_fob_button_functions.rb +45 -0
  27. data/lib/alula/resources/dcp/config/panel_options/minimum_pin_size.rb +23 -0
  28. data/lib/alula/resources/dcp/config/panel_options/panel_led_brightness.rb +23 -0
  29. data/lib/alula/resources/dcp/config/panel_options/panel_misc_options.rb +25 -0
  30. data/lib/alula/resources/dcp/config/panel_options/panel_options.rb +60 -0
  31. data/lib/alula/resources/dcp/config/panel_options/reportable_event_types_partitions.rb +38 -0
  32. data/lib/alula/resources/dcp/config/panel_options/reportable_event_types_system.rb +27 -0
  33. data/lib/alula/resources/dcp/config/panel_options/swinger_threshold.rb +24 -0
  34. data/lib/alula/resources/dcp/config/panel_options/system_options.rb +45 -0
  35. data/lib/alula/resources/dcp/config/panel_options/trouble_beep_suppress.rb +37 -0
  36. data/lib/alula/resources/dcp/config/panel_options/virtual_interface_options.rb +32 -0
  37. data/lib/alula/resources/dcp/config/panel_options.rb +57 -0
  38. data/lib/alula/resources/dcp/config/partitions/partition.rb +54 -0
  39. data/lib/alula/resources/dcp/{users_data/installer_pin.rb → config/partitions.rb} +6 -6
  40. data/lib/alula/resources/dcp/config/siren_data/siren.rb +59 -0
  41. data/lib/alula/resources/dcp/config/siren_data.rb +21 -0
  42. data/lib/alula/resources/dcp/config/synchronize.rb +1 -1
  43. data/lib/alula/resources/dcp/config/timers/entry_delays.rb +27 -0
  44. data/lib/alula/resources/dcp/config/timers/exit_delays.rb +27 -0
  45. data/lib/alula/resources/dcp/config/timers/sensor_supervision_time.rb +27 -0
  46. data/lib/alula/resources/dcp/config/timers/siren_timeout.rb +24 -0
  47. data/lib/alula/resources/dcp/config/timers.rb +33 -0
  48. data/lib/alula/resources/dcp/config/users_data/duress_pin.rb +24 -0
  49. data/lib/alula/resources/dcp/config/users_data/installer_pin.rb +25 -0
  50. data/lib/alula/resources/dcp/config/users_data/user.rb +104 -0
  51. data/lib/alula/resources/dcp/config/users_data.rb +25 -0
  52. data/lib/alula/resources/dcp/config.rb +10 -1
  53. data/lib/alula/version.rb +1 -1
  54. data/lib/alula.rb +66 -5
  55. metadata +44 -5
  56. data/lib/alula/resources/dcp/users_data/user.rb +0 -100
  57. data/lib/alula/resources/dcp/users_data.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4272ad9dbb49623ad0599bbd6a341c23b3ec55498ff1ddad91f1c2c0716e74ad
4
- data.tar.gz: 71995cc0e81d17122f9345e1f9e6da1bd57c2d3477150129c78cdb2738b32cde
3
+ metadata.gz: 34cf5c0ab11f5e83c244464f016e809a228e2c404cfd05322fcc3876681ab583
4
+ data.tar.gz: 702f818c5619c276030cb9b3bc75a780e97a58f3237a887de39c693196d42d2c
5
5
  SHA512:
6
- metadata.gz: 24f89a7f36b094153bf1c0895617d5968bd63e3cc7e2dbe86b68753dab66256861193f5167d37cea0003539d0b6d0bb058ce5d2b8cccc8093f3130c3f55e4403
7
- data.tar.gz: c0d78aa748dd7fe7f1c2976d83d87ab178531d4da548b7c26a5d5582667efd7b0c389fc3405022ba09e1a49aa81cd4249fcad3de41f0cf2a4987ecf178d1812f
6
+ metadata.gz: 3eae60166983ae4dc654739434694b5fa3bbc6e957e605f645763e08ef53caf6926ecc858d92b88804788d95d79316a3d6d8cec682d0d252a055f089342d74bf
7
+ data.tar.gz: eb340114013a2eebd35aba3794be2e1f6c6f65ea5be3aa691a3feb29bac1737e1be35906980b52f3b7204b8d849eb14c92481e3e89d3c2d10b97dd5b1daef2ac
data/VERSION.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | ------------| --------------------------------------------------------------------------- |
5
+ | v2.16.0 | 2025-06-18 | Add API config template and all DCP entities |
5
6
  | v2.15.1 | 2025-07-19 | Rename DCP-synchronizable fields to conform to the schema |
6
7
  | v2.15.0 | 2025-06-10 | Add users relationship to stations |
7
8
  | v2.14.0 | 2025-04-16 | Add camera reboot and format sd card procs |
data/bin/console CHANGED
@@ -47,4 +47,5 @@ def setup_client(token)
47
47
  c.video_api_key = ENV['VIDEO_API_DOMAIN']
48
48
  end
49
49
  end
50
+
50
51
  IRB.start(__FILE__)
@@ -4,7 +4,7 @@ module Alula
4
4
  attr_accessor :id, :raw_data, :values, :dirty_attributes, :errors, :links, :link_matchers, :rate_limit
5
5
 
6
6
  def self.class_name
7
- name.split("::")[-1]
7
+ name.split('::')[-1]
8
8
  end
9
9
 
10
10
  def initialize(id = nil, attributes = {})
@@ -89,21 +89,20 @@ module Alula
89
89
  # Fetch known attributes out of the object, collected into a Hash in camelCase format
90
90
  # Intended for eventually making its way back up to the API
91
91
  def as_json
92
- self.field_names.each_with_object({}) do |ruby_key, obj|
93
- key = Util.camelize(ruby_key)
94
- val = self.send(ruby_key)
95
-
96
- if self.date_fields.include?(ruby_key) && ![nil, ''].include?(val)
97
- if val.respond_to? :strftime
98
- obj[key] = val.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
99
- else
100
- obj[key] = val.to_s
101
- end
102
- elsif val.is_a? Alula::ObjectField
103
- obj[key] = val.as_json
104
- else
105
- obj[key] = val
106
- end
92
+ field_names.each_with_object({}) do |ruby_key, obj|
93
+ key = Util.camelize(ruby_key)
94
+ val = send(ruby_key)
95
+ obj[key] = if date_fields.include?(ruby_key) && ![nil, ''].include?(val)
96
+ if val.respond_to? :strftime
97
+ val.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
98
+ else
99
+ val.to_s
100
+ end
101
+ elsif val.is_a?(Alula::ObjectField) || val.is_a?(Alula::Dcp::ObjectField)
102
+ val.as_json
103
+ else
104
+ val
105
+ end
107
106
  end
108
107
  end
109
108
 
@@ -117,7 +116,7 @@ module Alula
117
116
  end
118
117
 
119
118
  # Remove blank values if creating a new record
120
- values = values.select{ |k, v| !v.nil? } unless persisted?
119
+ values = values.reject { |_k, v| v.nil? } unless persisted?
121
120
  values
122
121
  end
123
122
 
@@ -135,7 +134,7 @@ module Alula
135
134
  def cache_links(links)
136
135
  @links = links
137
136
 
138
- @link_matchers = links.each_pair.each_with_object([]) do |(type, link), collection|
137
+ @link_matchers = links.each_pair.each_with_object([]) do |(_type, link), collection|
139
138
  data = link['data']
140
139
  next if data.nil?
141
140
 
@@ -160,8 +159,6 @@ module Alula
160
159
  self.class
161
160
  end
162
161
 
163
- private
164
-
165
162
  class ModelErrors
166
163
  include Enumerable
167
164
 
@@ -1,14 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'byebug'
3
4
  module Alula
4
5
  module Dcp
5
6
  module ResourceAttributes
6
7
  def self.extended(base)
7
8
  base.class_eval do
9
+ class << self
10
+ attr_writer :has_svm
11
+
12
+ def has_svm
13
+ @has_svm ||= false
14
+ end
15
+ end
8
16
  @resource_path = nil
9
17
  @type = nil
10
18
  @http_methods = []
11
19
  @fields = {}
20
+
21
+ def mark_dirty(field_name, old_value, new_value)
22
+ @dirty_attributes << field_name if old_value != new_value
23
+ end
12
24
  end
13
25
  base.include(InstanceMethods)
14
26
  end
@@ -49,10 +61,69 @@ module Alula
49
61
  def field(field_name, **opts)
50
62
  @fields ||= {}
51
63
  @fields[field_name] = opts
64
+ return if has_svm && field_name != :index
65
+
66
+ json_key = Alula::Util.camelize(field_name)
67
+ instance_eval do
68
+ # Reader method for attribute
69
+ define_method(field_name) do
70
+ value = @values[json_key]
71
+ if opts[:type] == :date && ![nil, ''].include?(value)
72
+ begin
73
+ DateTime.parse(value)
74
+ rescue ArgumentError
75
+ value
76
+ end
77
+ elsif opts[:type] == :object && opts[:use] && !value.nil? && value.respond_to?(:each)
78
+ if opts[:use].superclass == Alula::Dcp::ObjectField
79
+ opts[:use].new({}, field_name, value)
80
+ elsif respond_to?(:device_id)
81
+ opts[:use].new(device_id, value)
82
+ else
83
+ opts[:use].new(nil, value)
84
+ end
85
+ elsif opts[:type] == :array && opts[:of] && !value.nil? && value.respond_to?(:each)
86
+ # Array of objects, each object is a new instance of the use class
87
+ value.each_with_object([]) do |item, collector|
88
+ collector << opts[:of].new(device_id, item)
89
+ end
90
+ elsif opts[:type] == :boolean
91
+ [true, 'true', 1, '1'].include? value
92
+ elsif opts[:type] == :number
93
+ value&.to_i
94
+ else
95
+ value
96
+ end
97
+ end
98
+
99
+ # Setter method
100
+ define_method("#{field_name}=") do |new_value|
101
+ #
102
+ # Coerce 'blank like' fields into a defined blank value
103
+ # This helps HTML form submissions. A blank text field comes through as an
104
+ # empty string, instead of a nil as the API validates for.
105
+ new_value = nil if new_value == ''
106
+
107
+ new_value = [true, 'true', 1, '1'].include? new_value if opts[:type] == :boolean
108
+
109
+ # Mark the attribute as dirty if the new value is different
110
+ mark_dirty(field_name, @values[json_key], new_value)
111
+ #
112
+ # Assign the new value (always assigned even if a duplicate)
113
+ @values[json_key] = new_value
114
+ end
115
+ end
52
116
  end
53
117
 
54
118
  def get_fields
55
- @fields
119
+ parent_fields = if superclass.respond_to?(:get_fields)
120
+ superclass.get_fields
121
+ else
122
+ {}
123
+ end
124
+
125
+ @fields ||= {}
126
+ parent_fields.merge(@fields)
56
127
  end
57
128
 
58
129
  def date_fields
@@ -79,7 +150,9 @@ module Alula
79
150
  end
80
151
 
81
152
  def param_key
82
- name.gsub('::', '_').downcase
153
+ # return only the last part of the class name, in snake_case
154
+ # e.g. Alula::Dcp::Config::PanelOptions becomes panel_options
155
+ Util.underscore(name.split('::')[-1])
83
156
  end
84
157
 
85
158
  private
@@ -145,8 +218,11 @@ module Alula
145
218
  extend ResourceAttributes
146
219
 
147
220
  # Assume properties is camel case
148
- def initialize(parent_field, properties = nil)
221
+ def initialize(dirty_attributes, parent_field, properties = nil)
222
+ @values = properties
149
223
  @parent_field = parent_field
224
+ @parent_dirty_attributes = dirty_attributes
225
+ @dirty_attributes = Set.new
150
226
  return unless properties
151
227
 
152
228
  valid_fields = self.class.get_fields
@@ -154,12 +230,18 @@ module Alula
154
230
  ruby_key = Alula::Util.underscore(key.to_s)
155
231
  next unless valid_fields.key?(ruby_key.to_sym)
156
232
 
157
- instance_variable_set("@#{ruby_key}", value)
158
- define_singleton_method(ruby_key) { instance_variable_get("@#{ruby_key}") }
159
- define_singleton_method("#{ruby_key}=") { |val| instance_variable_set("@#{ruby_key}", val) }
233
+ public_send("#{ruby_key}=", value)
160
234
  end
161
235
  end
162
236
 
237
+ def model_name
238
+ self.class
239
+ end
240
+
241
+ def present?
242
+ !@values.nil?
243
+ end
244
+
163
245
  def as_json
164
246
  field_names.each_with_object({}) do |ruby_key, obj|
165
247
  key = Util.camelize(ruby_key)
@@ -170,13 +252,52 @@ module Alula
170
252
  end
171
253
  end
172
254
 
255
+ #
256
+ # Take a hash of attributes and apply them to the model
257
+ def apply_attributes(attributes, merge_only: false)
258
+ res = attributes.each do |key, value|
259
+ next unless field_names.include?(key.to_sym)
260
+
261
+ opts = self.class.get_fields[key.to_sym]
262
+ json_key = Util.camelize(key.to_s)
263
+ if merge_only
264
+ # merge the value into the existing value
265
+ if value.is_a?(Array)
266
+ value.each_index do |index|
267
+ send(key)[index].apply_attributes(value[index], merge_only: true)
268
+ end
269
+ elsif value.is_a?(Hash) && send(key).respond_to?(:apply_attributes)
270
+ send(key).apply_attributes(value, merge_only: true)
271
+ elsif value.is_a?(Hash) && send(key).respond_to?(:each)
272
+ send("#{key}=", value[key])
273
+ elsif opts[:type] == :number
274
+ @values[json_key] = value.to_i
275
+ elsif opts[:type] == :boolean
276
+ @values[json_key] = [true, 'true', 1, '1'].include? value
277
+ else
278
+ @values[json_key] = value
279
+ end
280
+ else
281
+ send("#{key}=", value)
282
+ end
283
+ end
284
+ return @values if merge_only
285
+
286
+ res
287
+ end
288
+
173
289
  private
174
290
 
175
291
  def parse_value(value, ruby_key)
292
+ opts = self.class.get_fields[ruby_key.to_sym]
176
293
  if date_fields.include?(ruby_key) && ![nil, ''].include?(value)
177
294
  extract_date(value)
178
- elsif value.is_a? Alula::Dcp::ObjectField
295
+ elsif value.is_a?(Alula::Dcp::ObjectField) || value.is_a?(Alula::DcpResource)
179
296
  value.as_json
297
+ elsif opts[:type] == :boolean
298
+ [true, 'true', 1, '1'].include? value
299
+ elsif opts[:type] == :number
300
+ value&.to_i
180
301
  else
181
302
  value
182
303
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ class ResourceState
6
+ attr_reader :key
7
+
8
+ def initialize(resource, key)
9
+ @resource = resource
10
+ @key = key
11
+ end
12
+
13
+ def model_name
14
+ @resource.class
15
+ end
16
+
17
+ def apply_attributes(attributes, _opts = {})
18
+ attributes.each do |key, value|
19
+ send("#{key}=", value) if respond_to?("#{key}=")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def method_missing(name, *args)
26
+ field_name = name.to_s.sub(/=$/, '').to_sym
27
+ klass = get_discriminated_class(@resource.values)
28
+ if klass.get_fields.key?(field_name)
29
+ json_key = Util.camelize(field_name.to_s)
30
+ opts = klass.get_fields[field_name]
31
+ if name.to_s.end_with?('=')
32
+ # Setter method
33
+ define_singleton_method("#{field_name}=") do |new_value|
34
+ # transform keys inside new_value to camelCase
35
+ new_value = new_value.transform_keys { |k| Util.camelize(k.to_s) } if new_value.is_a?(Hash)
36
+ #
37
+ # Coerce 'blank like' fields into a defined blank value
38
+ # This helps HTML form submissions. A blank text field comes through as an
39
+ # empty string, instead of a nil as the API validates for.
40
+ new_value = nil if new_value == '' && opts[:type] != :string
41
+
42
+ new_value = [true, 'true', 1, '1'].include? new_value if opts[:type] == :boolean
43
+ new_value = new_value.to_i if opts[:type] == :number
44
+ if opts[:type] == :object && opts[:use]
45
+ obj_fields = opts[:use].get_fields
46
+ new_value.each do |k, v|
47
+ next unless obj_fields.key?(k.to_sym)
48
+
49
+ type = obj_fields[k.to_sym][:type]
50
+ case type
51
+ when :boolean
52
+ new_value[k] = [true, 'true', 1, '1'].include? v
53
+ when :number
54
+ new_value[k] = v.to_i
55
+ else
56
+ # For other types, just assign the value directly
57
+ new_value[k] = v
58
+ end
59
+ end
60
+ end
61
+ #
62
+ # Assign the new value (always assigned even if a duplicate)
63
+ @resource.values[@key][json_key] = new_value
64
+ end
65
+ send("#{field_name}=", *args)
66
+ else
67
+ define_singleton_method(field_name) do
68
+ value = @resource.values[@key][json_key]
69
+ return nil if value.nil?
70
+
71
+ # Regular type handling
72
+ if opts[:type] == :date && ![nil, ''].include?(value)
73
+ begin
74
+ DateTime.parse(value)
75
+ rescue ArgumentError
76
+ value
77
+ end
78
+ elsif opts[:type] == :object && opts[:use]
79
+ if opts[:use].superclass == Alula::Dcp::ObjectField
80
+ opts[:use].new({}, field_name, value)
81
+ elsif respond_to?(:device_id)
82
+ opts[:use].new(device_id, value['index'], value)
83
+ else
84
+ opts[:use].new(nil, nil, value)
85
+ end
86
+ elsif opts[:type] == :array && opts[:of]
87
+ # Array of objects, each object is a new instance of the use class
88
+ value.each_with_object([]) do |item, collector|
89
+ if item.is_a?(Hash) && item['index']
90
+ collector << opts[:of].new(device_id, item['index'], item)
91
+ else
92
+ collector << opts[:of].new(device_id, nil, item)
93
+ end
94
+ end
95
+ elsif opts[:type] == :boolean
96
+ [true, 'true', 1, '1'].include? value
97
+ elsif opts[:type] == :number
98
+ value&.to_i
99
+ else
100
+ value
101
+ end
102
+ end
103
+ send(field_name)
104
+ end
105
+ else
106
+ super
107
+ end
108
+ end
109
+
110
+ def respond_to_missing?(name, include_private = false)
111
+ field_name = name.to_s.sub(/=$/, '').to_sym
112
+ get_discriminated_class(@resource.values).get_fields.key?(field_name) || name == 'param_key' || super
113
+ end
114
+
115
+ def get_discriminated_class(attributes)
116
+ return @resource.class unless @resource.class.respond_to?(:discriminator) && @resource.class.discriminator
117
+
118
+ discriminator_field = @resource.class.discriminator
119
+ json_key = Util.camelize(discriminator_field.to_s)
120
+
121
+ # Try staged first, then value, then raw attributes
122
+ discriminator_value = if attributes['staged']&.key?(json_key)
123
+ attributes['staged'][json_key]
124
+ elsif attributes['value']&.key?(json_key)
125
+ attributes['value'][json_key]
126
+ else
127
+ attributes[json_key]
128
+ end
129
+ if discriminator_value && @resource.class.discriminate_by&.key?(discriminator_value)
130
+ Object.const_get(@resource.class.discriminate_by[discriminator_value])
131
+ else
132
+ @resource.class
133
+ end
134
+ end
135
+
136
+ def primitive?(resource)
137
+ resource.class.get_fields[:primitive]
138
+ end
139
+ end
140
+
141
+ class StagedState < ResourceState
142
+ def initialize(resource)
143
+ super(resource, 'staged')
144
+ end
145
+ end
146
+ class ValueState < ResourceState
147
+ def initialize(resource)
148
+ super(resource, 'value')
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ module StagedValueMethods
6
+ def self.included(base)
7
+ base.class_eval do
8
+ class << self
9
+ attr_writer :has_svm
10
+
11
+ def has_svm
12
+ @has_svm ||= true
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def staged
19
+ return nil if values['staged'].nil?
20
+
21
+ if primitive?(self)
22
+ # define setter method for staged value
23
+ define_singleton_method :staged= do |value|
24
+ values['staged'] = value
25
+ end
26
+ return values['staged']
27
+ end
28
+
29
+ @staged ||= StagedState.new(self)
30
+ end
31
+
32
+ def value
33
+ return nil if values['value'].nil?
34
+ if primitive?(self)
35
+ # define setter method for value
36
+ define_singleton_method :value= do |value|
37
+ values['value'] = value
38
+ end
39
+ return values['value']
40
+ end
41
+
42
+ @value ||= ValueState.new(self)
43
+ end
44
+
45
+ def primitive?(resource)
46
+ resource.class.get_fields[:primitive]
47
+ end
48
+ end
49
+ end
50
+ end