alula-ruby 2.6.3 → 2.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9018c7bf17b64dc46d9ecbce83df2fb8fa36313cc2baa70216c806e508ad8ebe
4
- data.tar.gz: 5552587d5c7d441b3fb48f7740c8c2e55f7e20b3e964f17465ba30913ddb7931
3
+ metadata.gz: fa567cfd5e7ba41e5ca4e6ff542ecc280d5dd516c2511d5d67784f733c20b0bc
4
+ data.tar.gz: 20b348feba6ea3f92e3f5b3fe033a0e750e2ba239a802d711438f6e5018297a3
5
5
  SHA512:
6
- metadata.gz: c8ddefb2c1f5094d244b0106cdfa28b7e6b7eea2e74059565d1163b3579967e3e8923b298efdeae1000cac9182ab00b916f396262f50fe766060fa0523fa2d81
7
- data.tar.gz: c1ff8064e6355d95f575dc779e42729e68327a02ae16606278b20c29b69ee36dea81e7f510113557ea7f94cd1cff582942d606caeefa841a8199354fd9c8af4f
6
+ metadata.gz: 0e9e19de05afc3e827bad08f3b760cfeda991d6690f8ba33f4a1c856e6f444cac4e37efb1a0c15fdc9648d257928652e5a8310dae6e4d1ff6ef46cf0816aa175
7
+ data.tar.gz: 7ec36038fcf83f0fc49ad0aa02193c79632f50ded31e0f58139b3e0bb98aab719f0f226a17d0eb24faca59547c6b820017a9b3bf70f7a0b48f1d8563abdf98ca
data/VERSION.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | ------------| --------------------------------------------------------------------------- |
5
+ | v2.8.0 | 2025-02-12 | Add ONVIF Features Selected for device. Add supports_onvif? method |
6
+ | v2.7.0 | 2025-02-05 | Add DCP client |
5
7
  | v2.6.3 | 2025-01-30 | Add alula_controllable_sim field to device |
6
8
  | v2.6.2 | 2025-01-30 | Fix Core API Docker container setup |
7
9
  | v2.6.1 | 2025-01-14 | Add ezviz? method to device |
data/bin/console CHANGED
@@ -47,5 +47,4 @@ def setup_client(token)
47
47
  c.video_api_key = ENV['VIDEO_API_DOMAIN']
48
48
  end
49
49
  end
50
-
51
50
  IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ # DCP Error Handler
6
+ module ErrorHandler
7
+ def handle_errors(response)
8
+ case response.http_status
9
+ when 300..399
10
+ # Should never see a 300-range response code
11
+ raise Alula::UnknownError,
12
+ "The Alula-Ruby gem encountered a response code of #{response.http_status}, that should not happen"
13
+ when 500..599
14
+ # Server errors, malformed data, etc
15
+ AlulaError.for_response(response)
16
+ when 400..499
17
+ # Validation errors usually
18
+ model_errors = Util.model_errors_from_dcp_response(response)
19
+ return AlulaError.for_response(response) unless model_errors
20
+
21
+ annotate_errors(model_errors)
22
+ false
23
+ else
24
+ raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ # Used to create a new list of DCP objects
6
+ class ListObject
7
+ include Enumerable
8
+
9
+ attr_reader :items, :type
10
+ attr_accessor :rate_limit
11
+
12
+ def initialize(list_type)
13
+ @type = list_type
14
+ @items = []
15
+ end
16
+
17
+ def length
18
+ items.length
19
+ end
20
+
21
+ def each(&block)
22
+ items.each { |item| block.call(item) }
23
+ end
24
+
25
+ def[](index)
26
+ @items[index]
27
+ end
28
+
29
+ def first
30
+ @items.first
31
+ end
32
+
33
+ def last
34
+ @items.last
35
+ end
36
+
37
+ def <<(item)
38
+ @items << item
39
+ end
40
+
41
+ def self.construct_from_response(device_id, klass, response, _opts)
42
+ # TODO: add pagination support if/when DCP supports it
43
+ list = ListObject.new(klass)
44
+ response.data.each do |sub_klass_string, items|
45
+ # if it's a real class, create an instance of it
46
+ # otherwise, log a warning and skip it
47
+ ruby_sub_klass_string = Util.upper_camelcase_from_camelcase(sub_klass_string)
48
+ sub_klass_name = "#{klass}::#{ruby_sub_klass_string}"
49
+ sub_klass = klass.const_defined?(ruby_sub_klass_string, false) ? Object.const_get(sub_klass_name) : nil
50
+ if sub_klass.nil?
51
+ Alula.logger.warn("No class found for #{sub_klass_name}")
52
+ next
53
+ end
54
+
55
+ sub_klass_list = []
56
+ items.each do |item|
57
+ sub_klass_list << sub_klass.new(device_id, item['index'], item)
58
+ end
59
+ list << sub_klass_list
60
+ # define getter method for each sub_klass
61
+ ruby_method = Util.underscore(sub_klass_string)
62
+ list.define_singleton_method(ruby_method) do
63
+ sub_klass_list
64
+ end
65
+ end
66
+ list.rate_limit = response.rate_limit
67
+
68
+ list
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ # DCP Record Meta
6
+ class Meta
7
+ attr_reader :last_recv_result, :last_recv_nak_reason, :last_recv_time, :recv_needed,
8
+ :last_write_time, :last_write_result, :last_write_nak_reason
9
+
10
+ def initialize(data)
11
+ data.each do |key, value|
12
+ ruby_key = Util.underscore(key)
13
+ instance_variable_set("@#{ruby_key}", value)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module Dcp
5
+ module ResourceAttributes
6
+ def self.extended(base)
7
+ base.class_eval do
8
+ @resource_path = nil
9
+ @type = nil
10
+ @http_methods = []
11
+ @fields = {}
12
+ end
13
+ base.include(InstanceMethods)
14
+ end
15
+
16
+ #
17
+ # Class methods for defining how fields and types work
18
+ # NOTE: We're not using real getters and setters here. I want the method signature
19
+ # to match how most other Ruby class configuration is done, and that is with
20
+ # simple methods that take params.
21
+ def resource_path(name)
22
+ @resource_path = name
23
+ end
24
+
25
+ def get_resource_path(index = nil)
26
+ return "#{@resource_path}/#{index}" if index
27
+
28
+ @resource_path
29
+ end
30
+
31
+ def type(type)
32
+ @type = type
33
+ end
34
+
35
+ def get_type
36
+ @type
37
+ end
38
+
39
+ def http_methods(methods)
40
+ @http_methods = methods
41
+ end
42
+
43
+ def get_http_methods
44
+ @http_methods
45
+ end
46
+
47
+ #
48
+ # Memoizes field names and options
49
+ def field(field_name, **opts)
50
+ @fields ||= {}
51
+ @fields[field_name] = opts
52
+ end
53
+
54
+ def get_fields
55
+ @fields
56
+ end
57
+
58
+ def date_fields
59
+ @date_fields ||=
60
+ get_fields.each_pair.each_with_object([]) do |(field_name, opts), collector|
61
+ collector << field_name if opts[:type].to_sym == :date
62
+ end
63
+ end
64
+
65
+ def filterable_fields
66
+ get_fields.each_pair.each_with_object([]) do |(field_name, opts), collector|
67
+ collector << field_name if opts[:filterable] == true
68
+ end
69
+ end
70
+
71
+ def sortable_fields
72
+ get_fields.each_pair.each_with_object([]) do |(field_name, opts), collector|
73
+ collector << field_name if opts[:sortable] == true
74
+ end
75
+ end
76
+
77
+ def field_names
78
+ get_fields.keys
79
+ end
80
+
81
+ def param_key
82
+ name.gsub('::', '_').downcase
83
+ end
84
+
85
+ private
86
+
87
+ def metaclass
88
+ class << self
89
+ self
90
+ end
91
+ end
92
+
93
+ def field_patchable?(opts, record_persisted)
94
+ user_role = Alula::Client.config.role
95
+
96
+ # different permission arrays are used for updates vs creates
97
+ perm_key = record_persisted == true ? :patchable_by : :creatable_by
98
+
99
+ return true if opts[perm_key].include? :all
100
+
101
+ opts[perm_key].include? user_role
102
+ end
103
+
104
+ #
105
+ # Extensions to allow instances to get at their class config, either raw or
106
+ # reduced to a useable format for a specific task.
107
+ module InstanceMethods
108
+ # Instance method extensions
109
+ def type(arg)
110
+ self.class.type(arg)
111
+ end
112
+
113
+ def get_type
114
+ self.class.get_type
115
+ end
116
+
117
+ def fields
118
+ self.class.get_fields
119
+ end
120
+
121
+ def field_names
122
+ self.class.field_names
123
+ end
124
+
125
+ def persisted?
126
+ return false unless respond_to?(:index)
127
+
128
+ !index.nil?
129
+ end
130
+
131
+ #
132
+ # Return the names of each field configured as a date type
133
+ def date_fields
134
+ self.class.date_fields
135
+ end
136
+
137
+ def to_key
138
+ key = respond_to?(:index) && index
139
+ key ? [key] : nil
140
+ end
141
+ end
142
+ end
143
+
144
+ class ObjectField
145
+ extend ResourceAttributes
146
+
147
+ # Assume properties is camel case
148
+ def initialize(parent_field, properties = nil)
149
+ @parent_field = parent_field
150
+ return unless properties
151
+
152
+ valid_fields = self.class.get_fields
153
+ properties.dup.each do |key, value|
154
+ ruby_key = Alula::Util.underscore(key.to_s)
155
+ next unless valid_fields.key?(ruby_key.to_sym)
156
+
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) }
160
+ end
161
+ end
162
+
163
+ def as_json
164
+ field_names.each_with_object({}) do |ruby_key, obj|
165
+ key = Util.camelize(ruby_key)
166
+ val = respond_to?(ruby_key) ? send(ruby_key) : nil
167
+ next if val.nil?
168
+
169
+ obj[key] = parse_value(val, ruby_key)
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def parse_value(value, ruby_key)
176
+ if date_fields.include?(ruby_key) && ![nil, ''].include?(value)
177
+ extract_date(value)
178
+ elsif value.is_a? Alula::Dcp::ObjectField
179
+ value.as_json
180
+ else
181
+ value
182
+ end
183
+ end
184
+
185
+ def extract_date(value)
186
+ if value.respond_to? :strftime
187
+ value.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
188
+ else
189
+ value.to_s
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Add real delete implementation when DCP supports it
4
+ module Alula
5
+ module DcpOperations
6
+ module Delete
7
+ def self.extended(base)
8
+ base.include(InstanceMethods)
9
+ end
10
+
11
+ module InstanceMethods
12
+ include Alula::Dcp::ErrorHandler
13
+ def delete
14
+ payload = {
15
+ data: {
16
+ id: id,
17
+ attributes: as_blank_patchable_json
18
+ }
19
+ }
20
+ response = Alula::Client.request(:patch, resource_url, payload)
21
+
22
+ return true if response.ok?
23
+
24
+ handle_errors(response)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Add real delete implementation when DCP supports it
4
+ # technically it does support delete staged at the moment
5
+ # but I would like to wait for the real delete.rb implementation to be added first
6
+ module Alula
7
+ module DcpOperations
8
+ module DeleteStaged
9
+ def self.extended(base)
10
+ base.include(InstanceMethods)
11
+ end
12
+
13
+ module InstanceMethods
14
+ include Alula::Dcp::ErrorHandler
15
+ def delete_staged
16
+ response = Alula::Client.request(:patch, "#{resource_url}/staged")
17
+
18
+ return true if response.ok?
19
+
20
+ handle_errors(response)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module DcpOperations
5
+ # Allows for the creation of a list of objects
6
+ module List
7
+ def self.extended(base)
8
+ base.include(InstanceMethods)
9
+ end
10
+
11
+ def method_missing(method, *args, &block)
12
+ QueryInterface.new(self).send(method, *args, &block)
13
+ end
14
+
15
+ def respond_to_missing?(method, include_private = false)
16
+ QueryInterface.new(self).respond_to?(method, include_private)
17
+ end
18
+
19
+ def list(device_id, filters = {}, opts = {})
20
+ response = Alula::Client.request(:get, resource_url(device_id), filters, opts)
21
+ if response.ok?
22
+ Alula::Dcp::ListObject.construct_from_response(device_id, self, response, opts)
23
+ else
24
+ error_class = AlulaError.for_response(response)
25
+ raise error_class
26
+ end
27
+ end
28
+
29
+ # Instance methods
30
+ module InstanceMethods
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module DcpOperations
5
+ # Allows for retrieving a single object by index
6
+ module Request
7
+ def self.extended(base)
8
+ base.include(InstanceMethods)
9
+ end
10
+
11
+ # Load a single model by Index
12
+ def retrieve(device_id, index, built_filters = {})
13
+ response = Alula::Client.request(:get, resource_url(device_id, index), built_filters, {})
14
+ if response.ok?
15
+ item = new(device_id, index, response.data)
16
+ item.rate_limit = response.rate_limit
17
+ item
18
+ else
19
+ error_class = AlulaError.for_response(response)
20
+ raise error_class
21
+ end
22
+ end
23
+
24
+ # Instance methods
25
+ module InstanceMethods
26
+ def request(method, url, params, opts)
27
+ Alula::Client.request(method, url, params, opts)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ module DcpOperations
5
+ # Module for saving a model
6
+ module Save
7
+ def self.extended(base)
8
+ base.include(InstanceMethods)
9
+ end
10
+
11
+ # Instance methods for saving a model
12
+ module InstanceMethods
13
+ include Alula::Dcp::ErrorHandler
14
+ def save
15
+ payload = as_patchable_json
16
+ response = Alula::Client.request(:patch, "#{resource_url}/staged", payload, {})
17
+
18
+ if response.ok?
19
+ construct_from(device_id, index, response.data)
20
+ return true
21
+ end
22
+ handle_errors(response)
23
+ end
24
+
25
+ def save!
26
+ save || raise(ValidationError, errors)
27
+ end
28
+
29
+ def create
30
+ payload = {
31
+ data: as_patchable_json
32
+ }
33
+
34
+ response = Alula::Client.request(:post, self.class.resource_url, payload, {})
35
+
36
+ if response.ok?
37
+ index = response.data['index']
38
+ construct_from(device_id, index, response.data)
39
+ return true
40
+ end
41
+
42
+ handle_errors(response)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end