ruby_home 0.2.1 → 0.2.6
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/.github/workflows/tests.yml +41 -0
- data/.gitignore +2 -2
- data/.rubocop.yml +2 -9
- data/.standard.yml +3 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +2 -2
- data/README.md +40 -77
- data/Rakefile +4 -4
- data/examples/air_purifier.rb +66 -0
- data/examples/air_quality_sensor.rb +119 -0
- data/examples/battery_service.rb +58 -0
- data/examples/carbon_dioxide_sensor.rb +78 -0
- data/examples/carbon_monoxide_sensor.rb +78 -0
- data/examples/contact_sensor.rb +66 -0
- data/examples/door.rb +48 -0
- data/examples/fan.rb +30 -0
- data/examples/fan_v2.rb +68 -0
- data/examples/garage_door_opener.rb +71 -0
- data/examples/heater_cooler.rb +92 -0
- data/examples/humidifier_dehumidifier.rb +88 -0
- data/examples/humidity_sensor.rb +62 -0
- data/examples/leak_sensor.rb +66 -0
- data/examples/light_sensor.rb +64 -0
- data/examples/lightbulb.rb +31 -0
- data/examples/lock_mechanism.rb +34 -0
- data/examples/motion_sensor.rb +66 -0
- data/examples/occupancy_sensor.rb +66 -0
- data/examples/outlet.rb +29 -0
- data/examples/security_system.rb +53 -0
- data/examples/smoke_sensor.rb +66 -0
- data/examples/switch.rb +16 -0
- data/examples/television.rb +73 -0
- data/examples/temperature_sensor.rb +62 -0
- data/examples/thermostat.rb +113 -0
- data/examples/window.rb +48 -0
- data/examples/window_covering.rb +72 -0
- data/lib/ruby_home.rb +21 -22
- data/lib/ruby_home/accessory.rb +1 -1
- data/lib/ruby_home/accessory_collection.rb +6 -6
- data/lib/ruby_home/accessory_info.rb +18 -18
- data/lib/ruby_home/characteristic.rb +13 -7
- data/lib/ruby_home/characteristic_collection.rb +13 -13
- data/lib/ruby_home/config/categories.yml +39 -0
- data/lib/ruby_home/config/characteristics.yml +190 -319
- data/lib/ruby_home/config/manual_characteristics.yml +278 -0
- data/lib/ruby_home/config/manual_services.yml +45 -0
- data/lib/ruby_home/config/services.yml +338 -304
- data/lib/ruby_home/configuration.rb +21 -2
- data/lib/ruby_home/device_id.rb +3 -3
- data/lib/ruby_home/dns/service.rb +4 -6
- data/lib/ruby_home/dns/text_record.rb +10 -10
- data/lib/ruby_home/errors.rb +1 -0
- data/lib/ruby_home/factories/characteristic_factory.rb +38 -37
- data/lib/ruby_home/factories/service_factory.rb +72 -72
- data/lib/ruby_home/factories/templates/characteristic_template.rb +6 -6
- data/lib/ruby_home/factories/templates/service_template.rb +12 -11
- data/lib/ruby_home/hap/crypto/chacha20poly1305.rb +2 -2
- data/lib/ruby_home/hap/crypto/hkdf.rb +4 -4
- data/lib/ruby_home/hap/crypto/session_key.rb +7 -7
- data/lib/ruby_home/hap/decrypter.rb +8 -8
- data/lib/ruby_home/hap/encrypter.rb +5 -6
- data/lib/ruby_home/hap/ev_response.rb +17 -17
- data/lib/ruby_home/hap/hap_request.rb +1 -1
- data/lib/ruby_home/hap/server.rb +4 -4
- data/lib/ruby_home/hap/server_handler.rb +8 -10
- data/lib/ruby_home/hap/session.rb +22 -22
- data/lib/ruby_home/hap/values/base_value.rb +3 -3
- data/lib/ruby_home/hap/values/bool_value.rb +4 -4
- data/lib/ruby_home/hap/values/float_value.rb +4 -4
- data/lib/ruby_home/hap/values/identify_value.rb +1 -1
- data/lib/ruby_home/hap/values/int32_value.rb +4 -4
- data/lib/ruby_home/hap/values/null_value.rb +1 -1
- data/lib/ruby_home/hap/values/string_value.rb +11 -11
- data/lib/ruby_home/hap/values/uint32_value.rb +5 -5
- data/lib/ruby_home/hap/values/uint8_value.rb +14 -14
- data/lib/ruby_home/hex_helper.rb +3 -3
- data/lib/ruby_home/http/application.rb +7 -7
- data/lib/ruby_home/http/controllers/accessories_controller.rb +3 -3
- data/lib/ruby_home/http/controllers/application_controller.rb +6 -6
- data/lib/ruby_home/http/controllers/characteristics_controller.rb +29 -29
- data/lib/ruby_home/http/controllers/identify_controller.rb +10 -10
- data/lib/ruby_home/http/controllers/pair_setups_controller.rb +19 -19
- data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +9 -9
- data/lib/ruby_home/http/controllers/pairings_controller.rb +6 -6
- data/lib/ruby_home/http/serializers/accessory_serializer.rb +5 -5
- data/lib/ruby_home/http/serializers/characteristic_serializer.rb +27 -19
- data/lib/ruby_home/http/serializers/characteristic_value_serializer.rb +5 -5
- data/lib/ruby_home/http/serializers/object_serializer.rb +1 -1
- data/lib/ruby_home/http/serializers/service_serializer.rb +19 -9
- data/lib/ruby_home/http/serializers/uuid_helper.rb +2 -2
- data/lib/ruby_home/http/services/session_notifier.rb +8 -8
- data/lib/ruby_home/http/services/start_srp_service.rb +3 -3
- data/lib/ruby_home/http/services/verify_finish_service.rb +22 -22
- data/lib/ruby_home/http/services/verify_srp_service.rb +22 -22
- data/lib/ruby_home/identifier_cache.rb +4 -5
- data/lib/ruby_home/password.rb +14 -14
- data/lib/ruby_home/persistable.rb +19 -12
- data/lib/ruby_home/service.rb +5 -2
- data/lib/ruby_home/service_collection.rb +1 -1
- data/lib/ruby_home/version.rb +1 -1
- data/rubyhome.gemspec +31 -31
- data/sbin/characteristic_generator.rb +22 -24
- data/sbin/service_generator.rb +38 -19
- metadata +85 -44
- data/.travis.yml +0 -31
@@ -1,10 +1,11 @@
|
|
1
1
|
module RubyHome
|
2
2
|
class ServiceTemplate
|
3
|
-
|
4
|
-
|
3
|
+
FILENAMES = %w[services.yml manual_services.yml].freeze
|
4
|
+
FILEPATHS = FILENAMES.map { |filename| File.join(__dir__, "..", "..", "config", filename) }.freeze
|
5
|
+
DATA = FILEPATHS.flat_map { |filepath| YAML.load_file(filepath) }.freeze
|
5
6
|
|
6
7
|
def self.all
|
7
|
-
@all ||= DATA.map { |data| new(data) }
|
8
|
+
@all ||= DATA.map { |data| new(**data) }
|
8
9
|
end
|
9
10
|
|
10
11
|
def self.find_by(options)
|
@@ -15,25 +16,25 @@ module RubyHome
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
|
-
def initialize(name:, description:, uuid:,
|
19
|
+
def initialize(name:, description:, uuid:, optional_characteristic_names:, required_characteristic_names:)
|
19
20
|
@name = name
|
20
21
|
@description = description
|
21
22
|
@uuid = uuid
|
22
|
-
@
|
23
|
-
@
|
23
|
+
@optional_characteristic_names = optional_characteristic_names
|
24
|
+
@required_characteristic_names = required_characteristic_names
|
24
25
|
end
|
25
26
|
|
26
|
-
attr_reader :name, :description, :uuid, :
|
27
|
+
attr_reader :name, :description, :uuid, :optional_characteristic_names, :required_characteristic_names
|
27
28
|
|
28
29
|
def optional_characteristics
|
29
|
-
@optional_characteristics ||=
|
30
|
-
CharacteristicTemplate.find_by(
|
30
|
+
@optional_characteristics ||= optional_characteristic_names.map do |name|
|
31
|
+
CharacteristicTemplate.find_by(name: name)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
35
|
def required_characteristics
|
35
|
-
@required_characteristics ||=
|
36
|
-
CharacteristicTemplate.find_by(
|
36
|
+
@required_characteristics ||= required_characteristic_names.map do |name|
|
37
|
+
CharacteristicTemplate.find_by(name: name)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
@@ -6,11 +6,11 @@ module RubyHome
|
|
6
6
|
@key = key
|
7
7
|
end
|
8
8
|
|
9
|
-
def encrypt(nonce, message, additional_data=nil)
|
9
|
+
def encrypt(nonce, message, additional_data = nil)
|
10
10
|
chacha20poly1305ietf.encrypt(nonce, message, additional_data)
|
11
11
|
end
|
12
12
|
|
13
|
-
def decrypt(nonce, ciphertext, additional_data=nil)
|
13
|
+
def decrypt(nonce, ciphertext, additional_data = nil)
|
14
14
|
chacha20poly1305ietf.decrypt(nonce, ciphertext, additional_data)
|
15
15
|
end
|
16
16
|
|
@@ -4,19 +4,19 @@ module RubyHome
|
|
4
4
|
module HAP
|
5
5
|
module Crypto
|
6
6
|
class HKDF
|
7
|
-
def initialize(salt:, info:
|
7
|
+
def initialize(salt:, info:)
|
8
8
|
@salt = salt
|
9
9
|
@info = info
|
10
10
|
end
|
11
11
|
|
12
12
|
def encrypt(source)
|
13
13
|
byte_string = convert_string_to_byte_string(source)
|
14
|
-
GEM_HKDF.new(byte_string, hkdf_opts).
|
14
|
+
GEM_HKDF.new(byte_string, hkdf_opts).read(BYTE_LENGTH)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
ALGORITHM =
|
19
|
+
ALGORITHM = "SHA512"
|
20
20
|
BYTE_LENGTH = 32
|
21
21
|
|
22
22
|
attr_reader :info, :salt, :source
|
@@ -33,7 +33,7 @@ module RubyHome
|
|
33
33
|
if string.encoding == Encoding::ASCII_8BIT
|
34
34
|
string
|
35
35
|
else
|
36
|
-
[string].pack(
|
36
|
+
[string].pack("H*")
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -16,15 +16,15 @@ module RubyHome
|
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
SALT = -"Control-Salt"
|
20
|
+
READ = -"Control-Read-Encryption-Key"
|
21
|
+
WRITE = -"Control-Write-Encryption-Key"
|
22
22
|
|
23
|
-
|
23
|
+
attr_reader :shared_secret
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def generate_shared_secret_key(info)
|
26
|
+
Crypto::HKDF.new(info: info, salt: SALT).encrypt(shared_secret)
|
27
|
+
end
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module RubyHome
|
2
2
|
module HAP
|
3
3
|
class Decrypter
|
4
|
-
AAD_LENGTH_BYTES = 2
|
5
|
-
AUTHENTICATE_TAG_LENGTH_BYTES = 16
|
6
|
-
NONCE_32_BIT_FIX_COMMENT_PART = [0].pack(
|
4
|
+
AAD_LENGTH_BYTES = 2
|
5
|
+
AUTHENTICATE_TAG_LENGTH_BYTES = 16
|
6
|
+
NONCE_32_BIT_FIX_COMMENT_PART = [0].pack("L").freeze
|
7
7
|
|
8
8
|
def initialize(key, count: 0)
|
9
9
|
@key = key
|
@@ -15,14 +15,14 @@ module RubyHome
|
|
15
15
|
read_pointer = 0
|
16
16
|
|
17
17
|
while read_pointer < data.length
|
18
|
-
little_endian_length_of_encrypted_data = data[read_pointer...read_pointer+AAD_LENGTH_BYTES]
|
19
|
-
length_of_encrypted_data = little_endian_length_of_encrypted_data.unpack1(
|
18
|
+
little_endian_length_of_encrypted_data = data[read_pointer...read_pointer + AAD_LENGTH_BYTES]
|
19
|
+
length_of_encrypted_data = little_endian_length_of_encrypted_data.unpack1("v")
|
20
20
|
read_pointer += AAD_LENGTH_BYTES
|
21
21
|
|
22
|
-
message = data[read_pointer...read_pointer+length_of_encrypted_data]
|
22
|
+
message = data[read_pointer...read_pointer + length_of_encrypted_data]
|
23
23
|
read_pointer += length_of_encrypted_data
|
24
24
|
|
25
|
-
auth_tag = data[read_pointer...read_pointer+AUTHENTICATE_TAG_LENGTH_BYTES]
|
25
|
+
auth_tag = data[read_pointer...read_pointer + AUTHENTICATE_TAG_LENGTH_BYTES]
|
26
26
|
read_pointer += AUTHENTICATE_TAG_LENGTH_BYTES
|
27
27
|
|
28
28
|
ciphertext = message + auth_tag
|
@@ -46,7 +46,7 @@ module RubyHome
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def nonce
|
49
|
-
NONCE_32_BIT_FIX_COMMENT_PART + [count].pack(
|
49
|
+
NONCE_32_BIT_FIX_COMMENT_PART + [count].pack("Q")
|
50
50
|
end
|
51
51
|
|
52
52
|
def chacha20poly1305ietf
|
@@ -2,8 +2,8 @@ module RubyHome
|
|
2
2
|
module HAP
|
3
3
|
class Session
|
4
4
|
class Encrypter
|
5
|
-
MAX_FRAME_LENGTH = 1024
|
6
|
-
NONCE_32_BIT_FIX_COMMENT_PART = [0].pack(
|
5
|
+
MAX_FRAME_LENGTH = 1024
|
6
|
+
NONCE_32_BIT_FIX_COMMENT_PART = [0].pack("L").freeze
|
7
7
|
|
8
8
|
def initialize(key, count: 0)
|
9
9
|
@key = key
|
@@ -17,9 +17,9 @@ module RubyHome
|
|
17
17
|
while read_pointer < data.length
|
18
18
|
encrypted_frame = ""
|
19
19
|
|
20
|
-
frame = data[read_pointer...read_pointer+MAX_FRAME_LENGTH]
|
20
|
+
frame = data[read_pointer...read_pointer + MAX_FRAME_LENGTH]
|
21
21
|
length_of_encrypted_data = frame.length
|
22
|
-
little_endian_length_of_encrypted_data = [length_of_encrypted_data].pack(
|
22
|
+
little_endian_length_of_encrypted_data = [length_of_encrypted_data].pack("v")
|
23
23
|
|
24
24
|
encrypted_frame += little_endian_length_of_encrypted_data
|
25
25
|
encrypted_frame += chacha20poly1305ietf.encrypt(nonce, frame, little_endian_length_of_encrypted_data)
|
@@ -43,7 +43,7 @@ module RubyHome
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def nonce
|
46
|
-
NONCE_32_BIT_FIX_COMMENT_PART + [count].pack(
|
46
|
+
NONCE_32_BIT_FIX_COMMENT_PART + [count].pack("Q")
|
47
47
|
end
|
48
48
|
|
49
49
|
def chacha20poly1305ietf
|
@@ -53,4 +53,3 @@ module RubyHome
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
|
-
|
@@ -13,28 +13,28 @@ module RubyHome
|
|
13
13
|
|
14
14
|
private
|
15
15
|
|
16
|
-
|
16
|
+
attr_reader :body, :session
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
CRLF = -"\x0d\x0a"
|
19
|
+
STATUS_LINE = -"EVENT/1.0 200 OK"
|
20
|
+
CONTENT_TYPE_LINE = -"Content-Type: application/hap+json"
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def content_length_line
|
23
|
+
"Content-Length: #{body.length}"
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
def send_header(io)
|
27
|
+
data = STATUS_LINE + CRLF
|
28
|
+
data << CONTENT_TYPE_LINE + CRLF
|
29
|
+
data << content_length_line + CRLF
|
30
|
+
data << CRLF
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
io.write(data)
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
def send_body(io)
|
36
|
+
io.write(body)
|
37
|
+
end
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
data/lib/ruby_home/hap/server.rb
CHANGED
@@ -4,11 +4,11 @@ module RubyHome
|
|
4
4
|
def run(sock)
|
5
5
|
session = Session.new(sock)
|
6
6
|
|
7
|
-
|
7
|
+
loop do
|
8
8
|
req = HAPRequest.new(@config, session: session)
|
9
9
|
res = HAPResponse.new(@config)
|
10
10
|
begin
|
11
|
-
|
11
|
+
loop do
|
12
12
|
break if sock.to_io.wait_readable(0.5)
|
13
13
|
break if @status != :Running
|
14
14
|
end
|
@@ -30,13 +30,13 @@ module RubyHome
|
|
30
30
|
res.set_error(ex)
|
31
31
|
rescue ::WEBrick::HTTPStatus::Status => ex
|
32
32
|
res.status = ex.code
|
33
|
-
rescue
|
33
|
+
rescue => ex
|
34
34
|
@logger.error(ex)
|
35
35
|
res.set_error(ex, true)
|
36
36
|
ensure
|
37
37
|
if req.request_line
|
38
38
|
if req.keep_alive? && res.keep_alive?
|
39
|
-
req.fixup
|
39
|
+
req.fixup
|
40
40
|
end
|
41
41
|
res.send_response(session)
|
42
42
|
access_log(@config, req, res)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module RubyHome
|
2
2
|
module HAP
|
3
3
|
class RackHandler < ::Rack::Handler::WEBrick
|
4
|
-
def self.run(app, options={})
|
4
|
+
def self.run(app, options = {})
|
5
5
|
@server = Server.new(options)
|
6
6
|
@server.mount "/", RackHandler, app
|
7
7
|
yield @server if block_given?
|
@@ -10,7 +10,7 @@ module RubyHome
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class ServerHandler
|
13
|
-
def initialize(configuration:
|
13
|
+
def initialize(configuration:)
|
14
14
|
@configuration = configuration
|
15
15
|
end
|
16
16
|
|
@@ -33,19 +33,19 @@ module RubyHome
|
|
33
33
|
{
|
34
34
|
Port: port,
|
35
35
|
Host: bind_address,
|
36
|
-
ServerSoftware:
|
36
|
+
ServerSoftware: "RubyHome",
|
37
37
|
Logger: server_logger,
|
38
38
|
AccessLog: []
|
39
39
|
}
|
40
40
|
end
|
41
41
|
|
42
42
|
def server_logger
|
43
|
-
if ENV[
|
44
|
-
WEBrick::Log
|
45
|
-
elsif ENV[
|
46
|
-
WEBrick::Log
|
43
|
+
if ENV["DEBUG"] == "debug"
|
44
|
+
WEBrick::Log.new($stdout, WEBrick::BasicLog::DEBUG)
|
45
|
+
elsif ENV["DEBUG"] == "info"
|
46
|
+
WEBrick::Log.new($stdout, WEBrick::BasicLog::INFO)
|
47
47
|
else
|
48
|
-
WEBrick::Log
|
48
|
+
WEBrick::Log.new("/dev/null", WEBrick::BasicLog::WARN)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -61,5 +61,3 @@ module RubyHome
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
65
|
-
|
@@ -54,35 +54,35 @@ module RubyHome
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
|
57
|
+
attr_reader :socket, :encrypter_class, :decrypter_class
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
def received_encrypted_request?
|
60
|
+
@received_encrypted_request ||= false
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
def encrypter
|
64
|
+
@_encrypter ||= encrypter_class.new(accessory_to_controller_key)
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
def encryption_time?
|
68
|
+
accessory_to_controller_key? && received_encrypted_request?
|
69
|
+
end
|
70
70
|
|
71
|
-
|
72
|
-
|
73
|
-
|
71
|
+
def decrypter
|
72
|
+
@_decrypter ||= decrypter_class.new(controller_to_accessory_key)
|
73
|
+
end
|
74
74
|
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
def decryption_time?
|
76
|
+
!!controller_to_accessory_key
|
77
|
+
end
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
79
|
+
def accessory_to_controller_key?
|
80
|
+
!!accessory_to_controller_key
|
81
|
+
end
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
def controller_to_accessory_key?
|
84
|
+
!!controller_to_accessory_key
|
85
|
+
end
|
86
86
|
end
|
87
87
|
end
|
88
88
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "facets/string/modulize"
|
2
2
|
|
3
3
|
module RubyHome
|
4
4
|
class BaseValue
|
@@ -8,7 +8,7 @@ module RubyHome
|
|
8
8
|
Object.const_get("::RubyHome::#{template.format.modulize}Value") || NullValue
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize(characteristic_template=nil, initial_value=nil)
|
11
|
+
def initialize(characteristic_template = nil, initial_value = nil)
|
12
12
|
@template = characteristic_template
|
13
13
|
@value = initial_value
|
14
14
|
end
|
@@ -25,6 +25,6 @@ module RubyHome
|
|
25
25
|
|
26
26
|
private
|
27
27
|
|
28
|
-
|
28
|
+
attr_reader :template
|
29
29
|
end
|
30
30
|
end
|
@@ -1,12 +1,12 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative "base_value"
|
2
2
|
|
3
3
|
module RubyHome
|
4
4
|
class BoolValue < BaseValue
|
5
5
|
REMAPPED_VALUES = {
|
6
|
-
|
6
|
+
"0" => false,
|
7
7
|
0 => false,
|
8
|
-
|
9
|
-
1 => true
|
8
|
+
"1" => true,
|
9
|
+
1 => true
|
10
10
|
}.freeze
|
11
11
|
|
12
12
|
def default
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative "base_value"
|
2
2
|
|
3
3
|
module RubyHome
|
4
4
|
class FloatValue < BaseValue
|
@@ -8,8 +8,8 @@ module RubyHome
|
|
8
8
|
|
9
9
|
private
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def minimum_value
|
12
|
+
template.constraints.fetch("MinimumValue", 0)
|
13
|
+
end
|
14
14
|
end
|
15
15
|
end
|