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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.hound.yml +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.travis.yml +21 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +36 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/rubyhome +16 -0
- data/bin/setup +8 -0
- data/lib/ruby_home.rb +6 -0
- data/lib/ruby_home/accessory_info.rb +99 -0
- data/lib/ruby_home/broadcast.rb +31 -0
- data/lib/ruby_home/config/characteristics.yml +1692 -0
- data/lib/ruby_home/config/services.yml +416 -0
- data/lib/ruby_home/device_id.rb +30 -0
- data/lib/ruby_home/dns/service.rb +44 -0
- data/lib/ruby_home/dns/text_record.rb +100 -0
- data/lib/ruby_home/factories/accessory_factory.rb +78 -0
- data/lib/ruby_home/factories/characteristic_factory.rb +57 -0
- data/lib/ruby_home/factories/templates/characteristic_template.rb +43 -0
- data/lib/ruby_home/factories/templates/service_template.rb +50 -0
- data/lib/ruby_home/hap/accessory.rb +26 -0
- data/lib/ruby_home/hap/characteristic.rb +60 -0
- data/lib/ruby_home/hap/hex_pad.rb +13 -0
- data/lib/ruby_home/hap/hkdf_encryption.rb +34 -0
- data/lib/ruby_home/hap/http_decryption.rb +58 -0
- data/lib/ruby_home/hap/http_encryption.rb +43 -0
- data/lib/ruby_home/hap/service.rb +38 -0
- data/lib/ruby_home/http/application.rb +28 -0
- data/lib/ruby_home/http/cache.rb +30 -0
- data/lib/ruby_home/http/controllers/accessories_controller.rb +19 -0
- data/lib/ruby_home/http/controllers/application_controller.rb +37 -0
- data/lib/ruby_home/http/controllers/characteristics_controller.rb +49 -0
- data/lib/ruby_home/http/controllers/pair_setups_controller.rb +146 -0
- data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +81 -0
- data/lib/ruby_home/http/controllers/pairings_controller.rb +38 -0
- data/lib/ruby_home/http/hap_request.rb +56 -0
- data/lib/ruby_home/http/hap_response.rb +57 -0
- data/lib/ruby_home/http/hap_server.rb +65 -0
- data/lib/ruby_home/http/serializers/accessory_serializer.rb +21 -0
- data/lib/ruby_home/http/serializers/characteristic_serializer.rb +26 -0
- data/lib/ruby_home/http/serializers/characteristic_value_serializer.rb +21 -0
- data/lib/ruby_home/http/serializers/object_serializer.rb +39 -0
- data/lib/ruby_home/http/serializers/service_serializer.rb +20 -0
- data/lib/ruby_home/identifier_cache.rb +59 -0
- data/lib/ruby_home/rack/handler/hap_server.rb +26 -0
- data/lib/ruby_home/tlv.rb +83 -0
- data/lib/ruby_home/tlv/bytes.rb +19 -0
- data/lib/ruby_home/tlv/int.rb +15 -0
- data/lib/ruby_home/tlv/utf8.rb +18 -0
- data/lib/ruby_home/version.rb +3 -0
- data/rubyhome.gemspec +43 -0
- data/sbin/characteristic_generator.rb +83 -0
- data/sbin/service_generator.rb +69 -0
- 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,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
|