alula-ruby 2.6.3 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9018c7bf17b64dc46d9ecbce83df2fb8fa36313cc2baa70216c806e508ad8ebe
4
- data.tar.gz: 5552587d5c7d441b3fb48f7740c8c2e55f7e20b3e964f17465ba30913ddb7931
3
+ metadata.gz: 41fa026d04be1c00a48b82beacd3d38f4e20ed611960477c8f86741d068f4d6e
4
+ data.tar.gz: 64f2dd9e84eb4b0afe9ce4a6e9eeae2e9f236999437d9f8a5bd120eab7ffecc3
5
5
  SHA512:
6
- metadata.gz: c8ddefb2c1f5094d244b0106cdfa28b7e6b7eea2e74059565d1163b3579967e3e8923b298efdeae1000cac9182ab00b916f396262f50fe766060fa0523fa2d81
7
- data.tar.gz: c1ff8064e6355d95f575dc779e42729e68327a02ae16606278b20c29b69ee36dea81e7f510113557ea7f94cd1cff582942d606caeefa841a8199354fd9c8af4f
6
+ metadata.gz: 70be4d9af3d5720c063710e0c76499d3aeff541a8ec5a1739f65b913cd57b3a3339cb5ec038650ecb144e77e36938cc889c7564d204ba2ec12039a867c6aa2ef
7
+ data.tar.gz: 637c4322fae7e3734f5ff382413aed172591c80841b3f4236d9c1b98cc72e6ab0c505517b870ccf2bf04b5209990b4645d03072bbce26f61c9ec162cb142e3d3
data/VERSION.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | ------------| --------------------------------------------------------------------------- |
5
+ | v2.7.0 | 2025-02-05 | Add DCP client |
5
6
  | v2.6.3 | 2025-01-30 | Add alula_controllable_sim field to device |
6
7
  | v2.6.2 | 2025-01-30 | Fix Core API Docker container setup |
7
8
  | 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