add hass integration support
This commit is contained in:
parent
2f66d38fba
commit
93604be82d
|
@ -1,7 +1,7 @@
|
||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
var sky = preload("res://assets/materials/sky.material")
|
var sky = preload ("res://assets/materials/sky.material")
|
||||||
var sky_passthrough = preload("res://assets/materials/sky_passthrough.material")
|
var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material")
|
||||||
|
|
||||||
@onready var environment: WorldEnvironment = $WorldEnvironment
|
@onready var environment: WorldEnvironment = $WorldEnvironment
|
||||||
@onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D
|
@onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D
|
||||||
|
@ -11,6 +11,8 @@ var sky_passthrough = preload("res://assets/materials/sky_passthrough.material")
|
||||||
@onready var menu = $Menu
|
@onready var menu = $Menu
|
||||||
@onready var keyboard = $Keyboard
|
@onready var keyboard = $Keyboard
|
||||||
|
|
||||||
|
var last_room = null
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# In case we're running on the headset, use the passthrough sky
|
# In case we're running on the headset, use the passthrough sky
|
||||||
if OS.get_name() == "Android":
|
if OS.get_name() == "Android":
|
||||||
|
@ -43,7 +45,7 @@ func _ready():
|
||||||
if action.name == "menu_button":
|
if action.name == "menu_button":
|
||||||
toggle_menu()
|
toggle_menu()
|
||||||
elif action.name == "by_button":
|
elif action.name == "by_button":
|
||||||
House.body.mini_view = !House.body.mini_view
|
House.body.mini_view=!House.body.mini_view
|
||||||
)
|
)
|
||||||
|
|
||||||
EventSystem.on_focus_in.connect(func(event):
|
EventSystem.on_focus_in.connect(func(event):
|
||||||
|
@ -51,7 +53,7 @@ func _ready():
|
||||||
return
|
return
|
||||||
|
|
||||||
add_child(keyboard)
|
add_child(keyboard)
|
||||||
keyboard.global_transform = menu.get_node("AnimationContainer/KeyboardPlace").global_transform
|
keyboard.global_transform=menu.get_node("AnimationContainer/KeyboardPlace").global_transform
|
||||||
)
|
)
|
||||||
|
|
||||||
EventSystem.on_focus_out.connect(func(event):
|
EventSystem.on_focus_out.connect(func(event):
|
||||||
|
@ -70,7 +72,7 @@ func toggle_menu():
|
||||||
if menu.show_menu == false:
|
if menu.show_menu == false:
|
||||||
remove_child(menu)
|
remove_child(menu)
|
||||||
|
|
||||||
func _emit_action(name: String, value, right_controller: bool = true):
|
func _emit_action(name: String, value, right_controller: bool=true):
|
||||||
var event = EventAction.new()
|
var event = EventAction.new()
|
||||||
event.name = name
|
event.name = name
|
||||||
event.value = value
|
event.value = value
|
||||||
|
@ -82,6 +84,19 @@ func _emit_action(name: String, value, right_controller: bool = true):
|
||||||
TYPE_FLOAT, TYPE_VECTOR2:
|
TYPE_FLOAT, TYPE_VECTOR2:
|
||||||
EventSystem.emit("action_value", event)
|
EventSystem.emit("action_value", event)
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
var room = House.body.find_room_at(camera.global_position)
|
||||||
|
|
||||||
|
if room != last_room:
|
||||||
|
if room:
|
||||||
|
print("Room changed to: ", room.name)
|
||||||
|
HomeApi.api.update_room(room.name)
|
||||||
|
last_room = room
|
||||||
|
else:
|
||||||
|
print("Room changed to: ", "outside")
|
||||||
|
HomeApi.api.update_room("outside")
|
||||||
|
last_room = null
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
if OS.get_name() != "Android":
|
if OS.get_name() != "Android":
|
||||||
|
|
||||||
|
@ -128,7 +143,7 @@ func vector_key_mapping(key_positive_x: int, key_negative_x: int, key_positive_y
|
||||||
elif Input.is_physical_key_pressed(key_negative_x):
|
elif Input.is_physical_key_pressed(key_negative_x):
|
||||||
x = -1
|
x = -1
|
||||||
|
|
||||||
var vec = Vector3(x, 0 , y)
|
var vec = Vector3(x, 0, y)
|
||||||
|
|
||||||
if vec:
|
if vec:
|
||||||
vec = vec.normalized()
|
vec = vec.normalized()
|
||||||
|
|
|
@ -47,7 +47,7 @@ shadow_enabled = true
|
||||||
[node name="XROrigin3D" type="XROrigin3D" parent="."]
|
[node name="XROrigin3D" type="XROrigin3D" parent="."]
|
||||||
|
|
||||||
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
|
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
|
||||||
transform = Transform3D(1, 1.8976e-10, 4.07454e-10, 6.76872e-11, 1, 2.08734e-08, -5.82077e-11, 1.09139e-11, 1, 0.0356618, 0.71033, 0.00564247)
|
transform = Transform3D(0.999992, 0.00357422, -0.00141238, -0.00357241, 0.999993, 0.00127761, 0.00141693, -0.00127253, 0.999998, 0.0356617, 0.71033, 0.00564247)
|
||||||
cull_mask = 524287
|
cull_mask = 524287
|
||||||
current = true
|
current = true
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,6 @@ func set_text(value: String, insert: bool=false):
|
||||||
overflow_index = _calculate_overflow_index()
|
overflow_index = _calculate_overflow_index()
|
||||||
focus_caret()
|
focus_caret()
|
||||||
|
|
||||||
print(overflow_index, " ", char_offset, " ", caret_position)
|
|
||||||
|
|
||||||
func get_display_text():
|
func get_display_text():
|
||||||
# In case all chars fit, return the whole text.
|
# In case all chars fit, return the whole text.
|
||||||
if overflow_index == - 1:
|
if overflow_index == - 1:
|
||||||
|
|
|
@ -3,6 +3,7 @@ extends Node
|
||||||
class_name CallbackMap
|
class_name CallbackMap
|
||||||
|
|
||||||
var callbacks := {}
|
var callbacks := {}
|
||||||
|
var single_callbacks: Array = []
|
||||||
|
|
||||||
func add(key: Variant, callback: Callable) -> void:
|
func add(key: Variant, callback: Callable) -> void:
|
||||||
_validate_key(key)
|
_validate_key(key)
|
||||||
|
@ -15,13 +16,9 @@ func add(key: Variant, callback: Callable) -> void:
|
||||||
func add_once(key: Variant, callback: Callable) -> void:
|
func add_once(key: Variant, callback: Callable) -> void:
|
||||||
_validate_key(key)
|
_validate_key(key)
|
||||||
|
|
||||||
var fn: Callable
|
single_callbacks.append(callback)
|
||||||
|
|
||||||
fn = func(args: Array):
|
add(key, callback)
|
||||||
remove(key, fn)
|
|
||||||
callback.callv(args)
|
|
||||||
|
|
||||||
add(key, fn)
|
|
||||||
|
|
||||||
func remove(key: Variant, callback: Callable) -> void:
|
func remove(key: Variant, callback: Callable) -> void:
|
||||||
_validate_key(key)
|
_validate_key(key)
|
||||||
|
@ -29,6 +26,9 @@ func remove(key: Variant, callback: Callable) -> void:
|
||||||
if callbacks.has(key):
|
if callbacks.has(key):
|
||||||
callbacks[key].erase(callback)
|
callbacks[key].erase(callback)
|
||||||
|
|
||||||
|
if single_callbacks.has(callback):
|
||||||
|
single_callbacks.erase(callback)
|
||||||
|
|
||||||
func call_key(key: Variant, args: Array) -> void:
|
func call_key(key: Variant, args: Array) -> void:
|
||||||
_validate_key(key)
|
_validate_key(key)
|
||||||
|
|
||||||
|
@ -36,5 +36,8 @@ func call_key(key: Variant, args: Array) -> void:
|
||||||
for callback in callbacks[key]:
|
for callback in callbacks[key]:
|
||||||
callback.callv(args)
|
callback.callv(args)
|
||||||
|
|
||||||
|
if single_callbacks.has(callback):
|
||||||
|
remove(key, callback)
|
||||||
|
|
||||||
func _validate_key(key: Variant):
|
func _validate_key(key: Variant):
|
||||||
assert(typeof(key) == TYPE_STRING || typeof(key) == TYPE_INT || typeof(key) == TYPE_FLOAT, "key must be a string or number")
|
assert(typeof(key) == TYPE_STRING||typeof(key) == TYPE_INT||typeof(key) == TYPE_FLOAT, "key must be a string or number")
|
||||||
|
|
28
lib/home_apis/hass_ws/handlers/auth.gd
Normal file
28
lib/home_apis/hass_ws/handlers/auth.gd
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const HASS_API = preload ("../hass.gd")
|
||||||
|
|
||||||
|
signal on_authenticated()
|
||||||
|
|
||||||
|
var api: HASS_API
|
||||||
|
var url: String
|
||||||
|
var token: String
|
||||||
|
|
||||||
|
var authenticated := false
|
||||||
|
|
||||||
|
func _init(hass: HASS_API, url: String, token: String):
|
||||||
|
self.api = hass
|
||||||
|
self.url = url
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
func handle_message(message):
|
||||||
|
match message["type"]:
|
||||||
|
"auth_required":
|
||||||
|
api.send_packet({"type": "auth", "access_token": self.token})
|
||||||
|
"auth_ok":
|
||||||
|
authenticated = true
|
||||||
|
on_authenticated.emit()
|
||||||
|
"auth_invalid":
|
||||||
|
EventSystem.notify("Failed to authenticate with Home Assistant. Check your token and try again.", EventNotify.Type.DANGER)
|
||||||
|
api.handle_disconnect()
|
||||||
|
|
||||||
|
func on_disconnect():
|
||||||
|
authenticated = false
|
19
lib/home_apis/hass_ws/handlers/integration.gd
Normal file
19
lib/home_apis/hass_ws/handlers/integration.gd
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const HASS_API = preload ("../hass.gd")
|
||||||
|
|
||||||
|
var api: HASS_API
|
||||||
|
var integration_exists: bool = false
|
||||||
|
|
||||||
|
func _init(hass: HASS_API):
|
||||||
|
self.api = hass
|
||||||
|
|
||||||
|
func on_connect():
|
||||||
|
var response = await api.send_request_packet({
|
||||||
|
"type": "immersive_home/register",
|
||||||
|
"device_id": OS.get_unique_id(),
|
||||||
|
"name": OS.get_model_name(),
|
||||||
|
"version": OS.get_version(),
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if response.status == Promise.Status.RESOLVED:
|
||||||
|
integration_exists = true
|
|
@ -1,5 +1,8 @@
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
const AuthHandler = preload ("./handlers/auth.gd")
|
||||||
|
const IntegrationHandler = preload ("./handlers/integration.gd")
|
||||||
|
|
||||||
signal on_connect()
|
signal on_connect()
|
||||||
signal on_disconnect()
|
signal on_disconnect()
|
||||||
var connected := false
|
var connected := false
|
||||||
|
@ -13,25 +16,32 @@ var request_timeout := 10.0
|
||||||
var url := ""
|
var url := ""
|
||||||
var token := ""
|
var token := ""
|
||||||
|
|
||||||
|
|
||||||
var LOG_MESSAGES := false
|
var LOG_MESSAGES := false
|
||||||
|
|
||||||
var authenticated := false
|
|
||||||
|
|
||||||
var id := 1
|
var id := 1
|
||||||
var entities: Dictionary = {}
|
var entities: Dictionary = {}
|
||||||
var entitiy_callbacks := CallbackMap.new()
|
var entitiy_callbacks := CallbackMap.new()
|
||||||
var packet_callbacks := CallbackMap.new()
|
var packet_callbacks := CallbackMap.new()
|
||||||
|
|
||||||
func _init(url := self.url, token := self.token):
|
var auth_handler: AuthHandler
|
||||||
|
var integration_handler: IntegrationHandler
|
||||||
|
|
||||||
|
func _init(url:=self.url, token:=self.token):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
|
auth_handler = AuthHandler.new(self, url, token)
|
||||||
|
integration_handler = IntegrationHandler.new(self)
|
||||||
|
|
||||||
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
|
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
|
||||||
connect_ws()
|
connect_ws()
|
||||||
|
|
||||||
|
auth_handler.on_authenticated.connect(func():
|
||||||
|
start_subscriptions()
|
||||||
|
)
|
||||||
|
|
||||||
func connect_ws():
|
func connect_ws():
|
||||||
if url == "" || token == "":
|
if url == ""||token == "":
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Connecting to %s" % url + "/api/websocket")
|
print("Connecting to %s" % url + "/api/websocket")
|
||||||
|
@ -71,38 +81,12 @@ func _process(delta):
|
||||||
func handle_packet(packet: Dictionary):
|
func handle_packet(packet: Dictionary):
|
||||||
if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
|
if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
|
||||||
|
|
||||||
if packet.type == "auth_required":
|
auth_handler.handle_message(packet)
|
||||||
send_packet({
|
|
||||||
"type": "auth",
|
|
||||||
"access_token": self.token
|
|
||||||
})
|
|
||||||
|
|
||||||
elif packet.type == "auth_ok":
|
if packet.has("id"):
|
||||||
authenticated = true
|
|
||||||
start_subscriptions()
|
|
||||||
|
|
||||||
elif packet.type == "auth_invalid":
|
|
||||||
EventSystem.notify("Failed to authenticate, invalid auth token", EventNotify.Type.DANGER)
|
|
||||||
print("Failed to authenticate, invalid auth token")
|
|
||||||
handle_disconnect()
|
|
||||||
else:
|
|
||||||
packet_callbacks.call_key(int(packet.id), [packet])
|
packet_callbacks.call_key(int(packet.id), [packet])
|
||||||
|
|
||||||
func start_subscriptions():
|
func start_subscriptions():
|
||||||
assert(authenticated, "Not authenticated")
|
|
||||||
|
|
||||||
# await send_request_packet({
|
|
||||||
# "type": "supported_features",
|
|
||||||
# "features": {
|
|
||||||
# "coalesce_messages": 1
|
|
||||||
# }
|
|
||||||
# })
|
|
||||||
|
|
||||||
# await send_request_packet({
|
|
||||||
# "type": "subscribe_events",
|
|
||||||
# "event_type": "state_changed"
|
|
||||||
# })
|
|
||||||
|
|
||||||
send_subscribe_packet({
|
send_subscribe_packet({
|
||||||
"type": "subscribe_entities"
|
"type": "subscribe_entities"
|
||||||
}, func(packet: Dictionary):
|
}, func(packet: Dictionary):
|
||||||
|
@ -111,13 +95,12 @@ func start_subscriptions():
|
||||||
|
|
||||||
if packet.event.has("a"):
|
if packet.event.has("a"):
|
||||||
for entity in packet.event.a.keys():
|
for entity in packet.event.a.keys():
|
||||||
entities[entity] = {
|
entities[entity]={
|
||||||
"state": packet.event.a[entity]["s"],
|
"state": packet.event.a[entity]["s"],
|
||||||
"attributes": packet.event.a[entity]["a"]
|
"attributes": packet.event.a[entity]["a"]
|
||||||
}
|
}
|
||||||
entitiy_callbacks.call_key(entity, [entities[entity]])
|
entitiy_callbacks.call_key(entity, [entities[entity]])
|
||||||
connected = true
|
handle_connect()
|
||||||
on_connect.emit()
|
|
||||||
|
|
||||||
if packet.event.has("c"):
|
if packet.event.has("c"):
|
||||||
for entity in packet.event.c.keys():
|
for entity in packet.event.c.keys():
|
||||||
|
@ -126,14 +109,19 @@ func start_subscriptions():
|
||||||
|
|
||||||
if packet.event.c[entity].has("+"):
|
if packet.event.c[entity].has("+"):
|
||||||
if packet.event.c[entity]["+"].has("s"):
|
if packet.event.c[entity]["+"].has("s"):
|
||||||
entities[entity]["state"] = packet.event.c[entity]["+"]["s"]
|
entities[entity]["state"]=packet.event.c[entity]["+"]["s"]
|
||||||
if packet.event.c[entity]["+"].has("a"):
|
if packet.event.c[entity]["+"].has("a"):
|
||||||
entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"], true)
|
entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"], true)
|
||||||
entitiy_callbacks.call_key(entity, [entities[entity]])
|
entitiy_callbacks.call_key(entity, [entities[entity]])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func handle_connect():
|
||||||
|
integration_handler.on_connect()
|
||||||
|
connected = true
|
||||||
|
on_connect.emit()
|
||||||
|
|
||||||
func handle_disconnect():
|
func handle_disconnect():
|
||||||
authenticated = false
|
auth_handler.on_disconnect()
|
||||||
set_process(false)
|
set_process(false)
|
||||||
on_disconnect.emit()
|
on_disconnect.emit()
|
||||||
|
|
||||||
|
@ -153,18 +141,15 @@ func send_subscribe_packet(packet: Dictionary, callback: Callable):
|
||||||
})
|
})
|
||||||
id += 1
|
id += 1
|
||||||
|
|
||||||
|
func send_request_packet(packet: Dictionary, ignore_initial:=false):
|
||||||
func send_request_packet(packet: Dictionary, ignore_initial := false):
|
|
||||||
packet.id = id
|
packet.id = id
|
||||||
id += 1
|
id += 1
|
||||||
|
|
||||||
send_packet(packet)
|
|
||||||
|
|
||||||
var promise = Promise.new(func(resolve: Callable, reject: Callable):
|
var promise = Promise.new(func(resolve: Callable, reject: Callable):
|
||||||
var fn: Callable
|
var fn: Callable
|
||||||
|
|
||||||
if ignore_initial:
|
if ignore_initial:
|
||||||
fn = func(packet: Dictionary):
|
fn=func(packet: Dictionary):
|
||||||
if packet.type == "event":
|
if packet.type == "event":
|
||||||
resolve.call(packet)
|
resolve.call(packet)
|
||||||
packet_callbacks.remove(packet.id, fn)
|
packet_callbacks.remove(packet.id, fn)
|
||||||
|
@ -173,7 +158,7 @@ func send_request_packet(packet: Dictionary, ignore_initial := false):
|
||||||
else:
|
else:
|
||||||
packet_callbacks.add_once(packet.id, resolve)
|
packet_callbacks.add_once(packet.id, resolve)
|
||||||
|
|
||||||
var timeout = Timer.new()
|
var timeout=Timer.new()
|
||||||
timeout.set_wait_time(request_timeout)
|
timeout.set_wait_time(request_timeout)
|
||||||
timeout.set_one_shot(true)
|
timeout.set_one_shot(true)
|
||||||
timeout.timeout.connect(func():
|
timeout.timeout.connect(func():
|
||||||
|
@ -187,8 +172,9 @@ func send_request_packet(packet: Dictionary, ignore_initial := false):
|
||||||
timeout.start()
|
timeout.start()
|
||||||
)
|
)
|
||||||
|
|
||||||
return await promise.settled
|
send_packet(packet)
|
||||||
|
|
||||||
|
return await promise.settled
|
||||||
|
|
||||||
func send_packet(packet: Dictionary):
|
func send_packet(packet: Dictionary):
|
||||||
if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
|
if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
|
||||||
|
@ -221,15 +207,13 @@ func get_state(entity: String):
|
||||||
return entities[entity]
|
return entities[entity]
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
func watch_state(entity: String, callback: Callable):
|
func watch_state(entity: String, callback: Callable):
|
||||||
entitiy_callbacks.add(entity, callback)
|
entitiy_callbacks.add(entity, callback)
|
||||||
|
|
||||||
return func():
|
return func():
|
||||||
entitiy_callbacks.remove(entity, callback)
|
entitiy_callbacks.remove(entity, callback)
|
||||||
|
|
||||||
|
func set_state(entity: String, state: String, attributes: Dictionary={}):
|
||||||
func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
|
||||||
var domain = entity.split(".")[0]
|
var domain = entity.split(".")[0]
|
||||||
var service: String
|
var service: String
|
||||||
|
|
||||||
|
@ -271,4 +255,12 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func update_room(room: String):
|
||||||
|
var response = await send_request_packet({
|
||||||
|
"type": "immersive_home/update",
|
||||||
|
"device_id": OS.get_unique_id(),
|
||||||
|
"room": room
|
||||||
|
})
|
||||||
|
|
||||||
|
if response.status == Promise.Status.RESOLVED:
|
||||||
|
print("Room updated")
|
Loading…
Reference in New Issue
Block a user