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 +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
|