ruby_home 0.1.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.hound.yml +2 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +9 -0
  6. data/.travis.yml +21 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +21 -0
  9. data/README.md +36 -0
  10. data/Rakefile +7 -0
  11. data/bin/console +14 -0
  12. data/bin/rubyhome +16 -0
  13. data/bin/setup +8 -0
  14. data/lib/ruby_home.rb +6 -0
  15. data/lib/ruby_home/accessory_info.rb +99 -0
  16. data/lib/ruby_home/broadcast.rb +31 -0
  17. data/lib/ruby_home/config/characteristics.yml +1692 -0
  18. data/lib/ruby_home/config/services.yml +416 -0
  19. data/lib/ruby_home/device_id.rb +30 -0
  20. data/lib/ruby_home/dns/service.rb +44 -0
  21. data/lib/ruby_home/dns/text_record.rb +100 -0
  22. data/lib/ruby_home/factories/accessory_factory.rb +78 -0
  23. data/lib/ruby_home/factories/characteristic_factory.rb +57 -0
  24. data/lib/ruby_home/factories/templates/characteristic_template.rb +43 -0
  25. data/lib/ruby_home/factories/templates/service_template.rb +50 -0
  26. data/lib/ruby_home/hap/accessory.rb +26 -0
  27. data/lib/ruby_home/hap/characteristic.rb +60 -0
  28. data/lib/ruby_home/hap/hex_pad.rb +13 -0
  29. data/lib/ruby_home/hap/hkdf_encryption.rb +34 -0
  30. data/lib/ruby_home/hap/http_decryption.rb +58 -0
  31. data/lib/ruby_home/hap/http_encryption.rb +43 -0
  32. data/lib/ruby_home/hap/service.rb +38 -0
  33. data/lib/ruby_home/http/application.rb +28 -0
  34. data/lib/ruby_home/http/cache.rb +30 -0
  35. data/lib/ruby_home/http/controllers/accessories_controller.rb +19 -0
  36. data/lib/ruby_home/http/controllers/application_controller.rb +37 -0
  37. data/lib/ruby_home/http/controllers/characteristics_controller.rb +49 -0
  38. data/lib/ruby_home/http/controllers/pair_setups_controller.rb +146 -0
  39. data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +81 -0
  40. data/lib/ruby_home/http/controllers/pairings_controller.rb +38 -0
  41. data/lib/ruby_home/http/hap_request.rb +56 -0
  42. data/lib/ruby_home/http/hap_response.rb +57 -0
  43. data/lib/ruby_home/http/hap_server.rb +65 -0
  44. data/lib/ruby_home/http/serializers/accessory_serializer.rb +21 -0
  45. data/lib/ruby_home/http/serializers/characteristic_serializer.rb +26 -0
  46. data/lib/ruby_home/http/serializers/characteristic_value_serializer.rb +21 -0
  47. data/lib/ruby_home/http/serializers/object_serializer.rb +39 -0
  48. data/lib/ruby_home/http/serializers/service_serializer.rb +20 -0
  49. data/lib/ruby_home/identifier_cache.rb +59 -0
  50. data/lib/ruby_home/rack/handler/hap_server.rb +26 -0
  51. data/lib/ruby_home/tlv.rb +83 -0
  52. data/lib/ruby_home/tlv/bytes.rb +19 -0
  53. data/lib/ruby_home/tlv/int.rb +15 -0
  54. data/lib/ruby_home/tlv/utf8.rb +18 -0
  55. data/lib/ruby_home/version.rb +3 -0
  56. data/rubyhome.gemspec +43 -0
  57. data/sbin/characteristic_generator.rb +83 -0
  58. data/sbin/service_generator.rb +69 -0
  59. metadata +352 -0
@@ -0,0 +1,100 @@
1
+ require 'dnssd/text_record'
2
+
3
+ module RubyHome
4
+ class TextRecord < DNSSD::TextRecord
5
+ def initialize(accessory_info:)
6
+ @accessory_info = accessory_info
7
+ super(to_hash)
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :accessory_info
13
+
14
+ def to_hash
15
+ {
16
+ 'c#' => current_configuration_number,
17
+ 'ci' => accessory_category_identifier,
18
+ 'ff' => feature_flags,
19
+ 'id' => device_id,
20
+ 'md' => model_name,
21
+ 'pv' => protocol_version,
22
+ 's#' => current_state_number,
23
+ 'sf' => status_flags
24
+ }
25
+ end
26
+
27
+ # Current configuration number. Required. Must update when an accessory,
28
+ # service, or characteristic is added or removed on the accessory server.
29
+ # Accessories must increment the config number after a firmware update. This
30
+ # must have a range of 1-4294967295 and wrap to 1 when it overflows. This
31
+ # value must persist across reboots, power cycles, etc.
32
+
33
+ def current_configuration_number
34
+ 1
35
+ end
36
+
37
+ # Accessory Category Identifier. Required. Indicates the category that best
38
+ # describes the primary function of the accessory. This must have a range of
39
+ # 1-65535.
40
+
41
+ def accessory_category_identifier
42
+ 2
43
+ end
44
+
45
+ # Feature flags (e.g. "0x3" for bits 0 and 1). Required if non-zero.
46
+
47
+ def feature_flags
48
+ 0
49
+ end
50
+
51
+ # Status flags (e.g. "0x04" for bit 3). Value should be an unsigned integer.
52
+ # Required if non-zero.
53
+
54
+ STATUS_FLAGS = {
55
+ PAIRED: 0,
56
+ NOT_PAIRED: 1,
57
+ }.freeze
58
+
59
+ def status_flags
60
+ if accessory_info.paired?
61
+ STATUS_FLAGS[:PAIRED]
62
+ else
63
+ STATUS_FLAGS[:NOT_PAIRED]
64
+ end
65
+ end
66
+
67
+ # Device ID (Device ID (page 36)) of the accessory. The Device ID must be
68
+ # formatted as "XX:XX:XX:XX:XX:XX", where "XX" is a hexadecimal string
69
+ # representing a byte. Required. This value is also used as the accessory's
70
+ # Pairing Identifier.
71
+
72
+ def device_id
73
+ accessory_info.device_id
74
+ end
75
+
76
+ # Model name of the accessory (e.g. "Device1,1"). Required.
77
+
78
+ def model_name
79
+ 'RubyHome'
80
+ end
81
+
82
+ # Protocol version string <major>.<minor> (e.g. "1.0"). Required if value is
83
+ # not "1.0". The client should check this before displaying an accessory to
84
+ # the user. If the major version is greater than the major version the client
85
+ # software was built to support, it should hide the accessory from the user. A
86
+ # change in the minor version indicates the protocol is still compatible. This
87
+ # mechanism allows future versions of the protocol to hide itself from older
88
+ # clients that may not know how to handle it.
89
+
90
+ def protocol_version
91
+ 1.0
92
+ end
93
+
94
+ # Current state number. Required. This must have a value of "1".
95
+
96
+ def current_state_number
97
+ 1
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,78 @@
1
+ require_relative '../hap/service'
2
+ require_relative '../hap/accessory'
3
+ require_relative 'templates/service_template'
4
+ require_relative 'templates/characteristic_template'
5
+
6
+ module RubyHome
7
+ class AccessoryFactory
8
+ def self.create(service_name, characteristics: {}, **options)
9
+ new(service_name, options, characteristics).create
10
+ end
11
+
12
+ def initialize(service_name, accessory_options, characteristic_options)
13
+ @service_name = service_name
14
+ @accessory_options = accessory_options
15
+ @characteristic_options = characteristic_options
16
+ end
17
+
18
+ def create
19
+ yield service if block_given?
20
+
21
+ create_accessory_information
22
+ service.save
23
+ create_required_characteristics
24
+ create_optional_characteristics
25
+
26
+ service
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :service_name, :accessory_options, :characteristic_options
32
+
33
+ def template
34
+ @template ||= ServiceTemplate.find_by(name: service_name.to_sym)
35
+ end
36
+
37
+ def create_accessory_information
38
+ unless service_name == :accessory_information
39
+ AccessoryFactory.create(:accessory_information, accessory_information_params)
40
+ end
41
+ end
42
+
43
+ def accessory_information_params
44
+ accessory_options.merge(accessory: service.accessory)
45
+ end
46
+
47
+ def create_required_characteristics
48
+ template.required_characteristics.map do |characteristic_template|
49
+ CharacteristicFactory.create(characteristic_template.name, service: service) do |characteristic|
50
+ value = characteristic_options[characteristic.name]
51
+ next unless value
52
+
53
+ characteristic.value = value
54
+ end
55
+ end
56
+ end
57
+
58
+ def create_optional_characteristics
59
+ template.optional_characteristics.map do |characteristic_template|
60
+ value = characteristic_options[characteristic_template.name]
61
+ next unless value
62
+
63
+ CharacteristicFactory.create(characteristic_template.name, service: service) do |characteristic|
64
+ characteristic.value = value
65
+ end
66
+ end
67
+ end
68
+
69
+ def service
70
+ @service ||= Service.new(service_params)
71
+ end
72
+
73
+ def service_params
74
+ accessory_options[:accessory] ||= Accessory.new
75
+ accessory_options.merge(template.to_hash)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../hap/service'
2
+ require_relative '../hap/accessory'
3
+
4
+ module RubyHome
5
+ class CharacteristicFactory
6
+ DEFAULT_VALUES = {
7
+ firmware_revision: '1.0',
8
+ identify: nil,
9
+ manufacturer: 'Default-Manufacturer',
10
+ model: 'Default-Model',
11
+ name: 'RubyHome',
12
+ serial_number: 'Default-SerialNumber',
13
+ }.freeze
14
+
15
+ def self.create(characteristic_name, options={}, &block)
16
+ new(characteristic_name, options).create(&block)
17
+ end
18
+
19
+ def initialize(characteristic_name, options)
20
+ @characteristic_name = characteristic_name
21
+ @options = options
22
+ end
23
+
24
+ def create
25
+ yield characteristic if block_given?
26
+
27
+ characteristic.save
28
+ characteristic
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :characteristic_name, :options
34
+
35
+ def template
36
+ @template ||= CharacteristicTemplate.find_by(name: characteristic_name.to_sym)
37
+ end
38
+
39
+ def characteristic
40
+ @characteristic ||= Characteristic.new(characteristic_params)
41
+ end
42
+
43
+ def characteristic_params
44
+ options[:value] ||= default_value
45
+ options.merge(template.to_hash)
46
+ end
47
+
48
+ def default_value
49
+ DEFAULT_VALUES.fetch(characteristic_name.to_sym) do
50
+ case template.format
51
+ when 'bool'
52
+ false
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ module RubyHome
2
+ class CharacteristicTemplate
3
+ FILEPATH = (File.dirname(__FILE__) + '/../../config/characteristics.yml').freeze
4
+ DATA = YAML.load_file(FILEPATH).freeze
5
+
6
+ def self.all
7
+ @@all ||= DATA.map { |data| new(data) }
8
+ end
9
+
10
+ def self.find_by(options)
11
+ all.find do |characteristic|
12
+ options.all? do |key, value|
13
+ characteristic.send(key) == value
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(name:, description:, uuid:, format:, unit:, permissions:, properties:, constraints:)
19
+ @name = name
20
+ @description = description
21
+ @uuid = uuid
22
+ @format = format
23
+ @unit = unit
24
+ @permissions = permissions
25
+ @properties = properties
26
+ @constraints = constraints
27
+ end
28
+
29
+ attr_reader :name, :description, :uuid, :format, :unit, :permissions, :properties, :constraints
30
+
31
+ def to_hash
32
+ {
33
+ name: name,
34
+ description: description,
35
+ uuid: uuid,
36
+ format: format,
37
+ unit: unit,
38
+ properties: properties,
39
+ }
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,50 @@
1
+ require_relative 'characteristic_template'
2
+
3
+ module RubyHome
4
+ class ServiceTemplate
5
+ FILEPATH = (File.dirname(__FILE__) + '/../../config/services.yml').freeze
6
+ DATA = YAML.load_file(FILEPATH).freeze
7
+
8
+ def self.all
9
+ @all ||= DATA.map { |data| new(data) }
10
+ end
11
+
12
+ def self.find_by(options)
13
+ all.find do |characteristic|
14
+ options.all? do |key, value|
15
+ characteristic.send(key) == value
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(name:, description:, uuid:, optional_characteristics_uuids:, required_characteristics_uuids:)
21
+ @name = name
22
+ @description = description
23
+ @uuid = uuid
24
+ @optional_characteristics_uuids = optional_characteristics_uuids
25
+ @required_characteristics_uuids = required_characteristics_uuids
26
+ end
27
+
28
+ attr_reader :name, :description, :uuid, :optional_characteristics_uuids, :required_characteristics_uuids
29
+
30
+ def optional_characteristics
31
+ @optional_characteristics ||= optional_characteristics_uuids.map do |uuid|
32
+ CharacteristicTemplate.find_by(uuid: uuid)
33
+ end
34
+ end
35
+
36
+ def required_characteristics
37
+ @required_characteristics ||= required_characteristics_uuids.map do |uuid|
38
+ CharacteristicTemplate.find_by(uuid: uuid)
39
+ end
40
+ end
41
+
42
+ def to_hash
43
+ {
44
+ name: name,
45
+ description: description,
46
+ uuid: uuid
47
+ }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ module RubyHome
2
+ class Accessory
3
+ def initialize
4
+ @services = []
5
+ end
6
+
7
+ attr_reader :id, :services
8
+ attr_writer :id
9
+
10
+ def characteristics
11
+ services.flat_map(&:characteristics)
12
+ end
13
+
14
+ def next_available_instance_id
15
+ (largest_instance_id || 0) + 1
16
+ end
17
+
18
+ def instance_ids
19
+ services.map(&:instance_id) + characteristics.map(&:instance_id)
20
+ end
21
+
22
+ def largest_instance_id
23
+ instance_ids.max
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ require 'wisper'
2
+
3
+ module RubyHome
4
+ class Characteristic
5
+ include Wisper::Publisher
6
+
7
+ PROPERTIES = {
8
+ 'cnotify' => 'ev',
9
+ 'read' => 'pr',
10
+ 'uncnotify' => nil,
11
+ 'write' => 'pw',
12
+ }.freeze
13
+
14
+ def initialize(uuid:, name:, description:, format:, unit:, properties:, service: , value: nil)
15
+ @uuid = uuid
16
+ @name = name
17
+ @description = description
18
+ @format = format
19
+ @unit = unit
20
+ @properties = properties
21
+ @service = service
22
+ @value = value
23
+ end
24
+
25
+ attr_reader :service, :value, :uuid, :name, :description, :format, :unit, :properties
26
+ attr_accessor :instance_id
27
+
28
+ def accessory
29
+ service.accessory
30
+ end
31
+
32
+ def accessory_id
33
+ accessory.id
34
+ end
35
+
36
+ def service_iid
37
+ service.instance_id
38
+ end
39
+
40
+ def value=(new_value)
41
+ @value = new_value
42
+ broadcast(:updated, new_value)
43
+ end
44
+
45
+ def inspect
46
+ {
47
+ name: name,
48
+ value: value,
49
+ accessory_id: accessory_id,
50
+ service_iid: service_iid,
51
+ instance_id: instance_id
52
+ }
53
+ end
54
+
55
+ def save
56
+ IdentifierCache.add_accessory(accessory)
57
+ IdentifierCache.add_characteristic(self)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ module RubyHome
2
+ module HAP
3
+ module HexPad
4
+ def self.pad(input, pad_length: 24)
5
+ [
6
+ input
7
+ .unpack1('H*')
8
+ .rjust(pad_length, '0')
9
+ ].pack('H*')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ require 'rbnacl/libsodium'
2
+
3
+ module RubyHome
4
+ module HAP
5
+ class HKDFEncryption
6
+ def initialize(salt:, info: )
7
+ @salt = salt
8
+ @info = info
9
+ end
10
+
11
+ def encrypt(source)
12
+ HKDF.new(source, hkdf_opts).next_bytes(BYTE_LENGTH)
13
+ end
14
+
15
+ private
16
+
17
+ BYTE_LENGTH = 32
18
+
19
+ attr_reader :info, :salt, :source
20
+
21
+ def hkdf_opts
22
+ {
23
+ algorithm: algorithm,
24
+ info: info,
25
+ salt: salt
26
+ }
27
+ end
28
+
29
+ def algorithm
30
+ 'SHA512'
31
+ end
32
+ end
33
+ end
34
+ end