alula-ruby 2.6.3 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION.md +2 -0
- data/bin/console +0 -1
- data/lib/alula/dcp/error_handler.rb +29 -0
- data/lib/alula/dcp/list_object.rb +72 -0
- data/lib/alula/dcp/meta.rb +18 -0
- data/lib/alula/dcp/resource_attributes.rb +194 -0
- data/lib/alula/dcp_operations/delete.rb +29 -0
- data/lib/alula/dcp_operations/delete_staged.rb +25 -0
- data/lib/alula/dcp_operations/list.rb +34 -0
- data/lib/alula/dcp_operations/request.rb +32 -0
- data/lib/alula/dcp_operations/save.rb +47 -0
- data/lib/alula/dcp_resource.rb +238 -0
- data/lib/alula/errors.rb +16 -6
- data/lib/alula/helpers/device_helpers/program_id_helper.rb +9 -0
- data/lib/alula/resource_attributes.rb +5 -5
- data/lib/alula/resources/dcp/base_resource.rb +43 -0
- data/lib/alula/resources/dcp/config/synchronize.rb +53 -0
- data/lib/alula/resources/dcp/config.rb +8 -0
- data/lib/alula/resources/dcp/users_data/installer_pin.rb +21 -0
- data/lib/alula/resources/dcp/users_data/user.rb +100 -0
- data/lib/alula/resources/dcp/users_data.rb +13 -0
- data/lib/alula/resources/device.rb +1 -1
- data/lib/alula/singleton_dcp_command_resource.rb +100 -0
- data/lib/alula/util.rb +24 -7
- data/lib/alula/version.rb +1 -1
- data/lib/alula.rb +18 -1
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa567cfd5e7ba41e5ca4e6ff542ecc280d5dd516c2511d5d67784f733c20b0bc
|
4
|
+
data.tar.gz: 20b348feba6ea3f92e3f5b3fe033a0e750e2ba239a802d711438f6e5018297a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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
|