alula-ruby 2.15.1 → 2.16.1

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 +2 -0
  3. data/bin/console +1 -0
  4. data/lib/alula/api_resource.rb +17 -20
  5. data/lib/alula/dcp/resource_attributes.rb +127 -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: f3bf8e3477ce10c3045fe47f7bee7a445749338a08c22e49eec2ff49cb83e5b7
4
+ data.tar.gz: de5f43338748af6a94922dcbb0eb9dd73bc3565cacbbce1a17f314906c9e97e0
5
5
  SHA512:
6
- metadata.gz: 24f89a7f36b094153bf1c0895617d5968bd63e3cc7e2dbe86b68753dab66256861193f5167d37cea0003539d0b6d0bb058ce5d2b8cccc8093f3130c3f55e4403
7
- data.tar.gz: c0d78aa748dd7fe7f1c2976d83d87ab178531d4da548b7c26a5d5582667efd7b0c389fc3405022ba09e1a49aa81cd4249fcad3de41f0cf2a4987ecf178d1812f
6
+ metadata.gz: f6af1c562cce55dc4b361bb3ee2c36200b41e592302c50fbb935d5cd318c4db9aae3903631e8c48b4827016283b1497a437089aca4a17cc63756fc76a3f20402
7
+ data.tar.gz: 5edda7ca58669ea7343659ffd21474841def738f544e04d3597c133c4409db33fb914aa79b0edd5e4bce7f2170bad8a8aa72ab80943f7cc32584e5d7ffbe0a1f
data/VERSION.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | ------------| --------------------------------------------------------------------------- |
5
+ | v2.16.1 | 2025-06-24 | Remove byebug usages |
6
+ | v2.16.0 | 2025-06-18 | Add API config template and all DCP entities |
5
7
  | v2.15.1 | 2025-07-19 | Rename DCP-synchronizable fields to conform to the schema |
6
8
  | v2.15.0 | 2025-06-10 | Add users relationship to stations |
7
9
  | 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
 
@@ -5,10 +5,21 @@ module Alula
5
5
  module ResourceAttributes
6
6
  def self.extended(base)
7
7
  base.class_eval do
8
+ class << self
9
+ attr_writer :has_svm
10
+
11
+ def has_svm
12
+ @has_svm ||= false
13
+ end
14
+ end
8
15
  @resource_path = nil
9
16
  @type = nil
10
17
  @http_methods = []
11
18
  @fields = {}
19
+
20
+ def mark_dirty(field_name, old_value, new_value)
21
+ @dirty_attributes << field_name if old_value != new_value
22
+ end
12
23
  end
13
24
  base.include(InstanceMethods)
14
25
  end
@@ -49,10 +60,69 @@ module Alula
49
60
  def field(field_name, **opts)
50
61
  @fields ||= {}
51
62
  @fields[field_name] = opts
63
+ return if has_svm && field_name != :index
64
+
65
+ json_key = Alula::Util.camelize(field_name)
66
+ instance_eval do
67
+ # Reader method for attribute
68
+ define_method(field_name) do
69
+ value = @values[json_key]
70
+ if opts[:type] == :date && ![nil, ''].include?(value)
71
+ begin
72
+ DateTime.parse(value)
73
+ rescue ArgumentError
74
+ value
75
+ end
76
+ elsif opts[:type] == :object && opts[:use] && !value.nil? && value.respond_to?(:each)
77
+ if opts[:use].superclass == Alula::Dcp::ObjectField
78
+ opts[:use].new({}, field_name, value)
79
+ elsif respond_to?(:device_id)
80
+ opts[:use].new(device_id, value)
81
+ else
82
+ opts[:use].new(nil, value)
83
+ end
84
+ elsif opts[:type] == :array && opts[:of] && !value.nil? && value.respond_to?(:each)
85
+ # Array of objects, each object is a new instance of the use class
86
+ value.each_with_object([]) do |item, collector|
87
+ collector << opts[:of].new(device_id, item)
88
+ end
89
+ elsif opts[:type] == :boolean
90
+ [true, 'true', 1, '1'].include? value
91
+ elsif opts[:type] == :number
92
+ value&.to_i
93
+ else
94
+ value
95
+ end
96
+ end
97
+
98
+ # Setter method
99
+ define_method("#{field_name}=") do |new_value|
100
+ #
101
+ # Coerce 'blank like' fields into a defined blank value
102
+ # This helps HTML form submissions. A blank text field comes through as an
103
+ # empty string, instead of a nil as the API validates for.
104
+ new_value = nil if new_value == ''
105
+
106
+ new_value = [true, 'true', 1, '1'].include? new_value if opts[:type] == :boolean
107
+
108
+ # Mark the attribute as dirty if the new value is different
109
+ mark_dirty(field_name, @values[json_key], new_value)
110
+ #
111
+ # Assign the new value (always assigned even if a duplicate)
112
+ @values[json_key] = new_value
113
+ end
114
+ end
52
115
  end
53
116
 
54
117
  def get_fields
55
- @fields
118
+ parent_fields = if superclass.respond_to?(:get_fields)
119
+ superclass.get_fields
120
+ else
121
+ {}
122
+ end
123
+
124
+ @fields ||= {}
125
+ parent_fields.merge(@fields)
56
126
  end
57
127
 
58
128
  def date_fields
@@ -79,7 +149,9 @@ module Alula
79
149
  end
80
150
 
81
151
  def param_key
82
- name.gsub('::', '_').downcase
152
+ # return only the last part of the class name, in snake_case
153
+ # e.g. Alula::Dcp::Config::PanelOptions becomes panel_options
154
+ Util.underscore(name.split('::')[-1])
83
155
  end
84
156
 
85
157
  private
@@ -145,8 +217,11 @@ module Alula
145
217
  extend ResourceAttributes
146
218
 
147
219
  # Assume properties is camel case
148
- def initialize(parent_field, properties = nil)
220
+ def initialize(dirty_attributes, parent_field, properties = nil)
221
+ @values = properties
149
222
  @parent_field = parent_field
223
+ @parent_dirty_attributes = dirty_attributes
224
+ @dirty_attributes = Set.new
150
225
  return unless properties
151
226
 
152
227
  valid_fields = self.class.get_fields
@@ -154,12 +229,18 @@ module Alula
154
229
  ruby_key = Alula::Util.underscore(key.to_s)
155
230
  next unless valid_fields.key?(ruby_key.to_sym)
156
231
 
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) }
232
+ public_send("#{ruby_key}=", value)
160
233
  end
161
234
  end
162
235
 
236
+ def model_name
237
+ self.class
238
+ end
239
+
240
+ def present?
241
+ !@values.nil?
242
+ end
243
+
163
244
  def as_json
164
245
  field_names.each_with_object({}) do |ruby_key, obj|
165
246
  key = Util.camelize(ruby_key)
@@ -170,13 +251,52 @@ module Alula
170
251
  end
171
252
  end
172
253
 
254
+ #
255
+ # Take a hash of attributes and apply them to the model
256
+ def apply_attributes(attributes, merge_only: false)
257
+ res = attributes.each do |key, value|
258
+ next unless field_names.include?(key.to_sym)
259
+
260
+ opts = self.class.get_fields[key.to_sym]
261
+ json_key = Util.camelize(key.to_s)
262
+ if merge_only
263
+ # merge the value into the existing value
264
+ if value.is_a?(Array)
265
+ value.each_index do |index|
266
+ send(key)[index].apply_attributes(value[index], merge_only: true)
267
+ end
268
+ elsif value.is_a?(Hash) && send(key).respond_to?(:apply_attributes)
269
+ send(key).apply_attributes(value, merge_only: true)
270
+ elsif value.is_a?(Hash) && send(key).respond_to?(:each)
271
+ send("#{key}=", value[key])
272
+ elsif opts[:type] == :number
273
+ @values[json_key] = value.to_i
274
+ elsif opts[:type] == :boolean
275
+ @values[json_key] = [true, 'true', 1, '1'].include? value
276
+ else
277
+ @values[json_key] = value
278
+ end
279
+ else
280
+ send("#{key}=", value)
281
+ end
282
+ end
283
+ return @values if merge_only
284
+
285
+ res
286
+ end
287
+
173
288
  private
174
289
 
175
290
  def parse_value(value, ruby_key)
291
+ opts = self.class.get_fields[ruby_key.to_sym]
176
292
  if date_fields.include?(ruby_key) && ![nil, ''].include?(value)
177
293
  extract_date(value)
178
- elsif value.is_a? Alula::Dcp::ObjectField
294
+ elsif value.is_a?(Alula::Dcp::ObjectField) || value.is_a?(Alula::DcpResource)
179
295
  value.as_json
296
+ elsif opts[:type] == :boolean
297
+ [true, 'true', 1, '1'].include? value
298
+ elsif opts[:type] == :number
299
+ value&.to_i
180
300
  else
181
301
  value
182
302
  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