add hass integration support

This commit is contained in:
Nitwel 2024-03-11 18:09:50 +01:00
parent 2f66d38fba
commit 93604be82d
8 changed files with 121 additions and 66 deletions

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -154,4 +154,4 @@ func _on_entity_click(entity_name):
func clear_menu(): func clear_menu():
for child in devices_node.get_children(): for child in devices_node.get_children():
devices_node.remove_child(child) devices_node.remove_child(child)
child.queue_free() child.queue_free()

View File

@ -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):
remove(key, fn)
callback.callv(args)
add(key, fn) add(key, callback)
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")

View 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

View 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

View File

@ -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")