ruby_home 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|