diff --git a/main.gd b/main.gd index 0ca4af0..478168c 100644 --- a/main.gd +++ b/main.gd @@ -3,9 +3,9 @@ extends Node3D var sky = preload("res://assets/materials/sky.material") var sky_passthrough = preload("res://assets/materials/sky_passthrough.material") -@export var passthrough: bool = true @onready var environment: WorldEnvironment = $WorldEnvironment func _ready(): - if passthrough: - environment.environment.sky.set_material(sky_passthrough) \ No newline at end of file + # In case we're running on the headset, use the passthrough sky + if OS.get_name() == "Android": + environment.environment.sky.set_material(sky_passthrough) diff --git a/main.tscn b/main.tscn index fd5f1b5..c3d7c33 100644 --- a/main.tscn +++ b/main.tscn @@ -28,7 +28,6 @@ ambient_light_sky_contribution = 0.72 [node name="Main" type="Node3D"] transform = Transform3D(1, -0.000296142, 0.000270963, 0.000296143, 1, -4.61078e-06, -0.000270962, 4.67014e-06, 1, 0, 0, 0) script = ExtResource("1_uvrd4") -passthrough = false [node name="XROrigin3D" type="XROrigin3D" parent="."] diff --git a/promise/LICENSE b/promise/LICENSE new file mode 100644 index 0000000..3155814 --- /dev/null +++ b/promise/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 TheWalruzz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/promise/promise.gd b/promise/promise.gd new file mode 100644 index 0000000..c25a84f --- /dev/null +++ b/promise/promise.gd @@ -0,0 +1,165 @@ +extends RefCounted +class_name Promise + + +enum Status { + RESOLVED, + REJECTED +} + + +signal settled(status: PromiseResult) +signal resolved(value: Variant) +signal rejected(reason: Rejection) + + +## Generic rejection reason +const PROMISE_REJECTED := "Promise rejected" + + +var is_settled := false + + +func _init(callable: Callable): + resolved.connect( + func(value: Variant): + is_settled = true + settled.emit(PromiseResult.new(Status.RESOLVED, value)), + CONNECT_ONE_SHOT + ) + rejected.connect( + func(rejection: Rejection): + is_settled = true + settled.emit(PromiseResult.new(Status.REJECTED, rejection)), + CONNECT_ONE_SHOT + ) + + callable.call_deferred( + func(value: Variant): + if not is_settled: + resolved.emit(value), + func(rejection: Rejection): + if not is_settled: + rejected.emit(rejection) + ) + + +func then(resolved_callback: Callable) -> Promise: + resolved.connect( + resolved_callback, + CONNECT_ONE_SHOT + ) + return self + + +func catch(rejected_callback: Callable) -> Promise: + rejected.connect( + rejected_callback, + CONNECT_ONE_SHOT + ) + return self + + +static func from(input_signal: Signal) -> Promise: + return Promise.new( + func(resolve: Callable, _reject: Callable): + var number_of_args := input_signal.get_object().get_signal_list() \ + .filter(func(signal_info: Dictionary) -> bool: return signal_info["name"] == input_signal.get_name()) \ + .map(func(signal_info: Dictionary) -> int: return signal_info["args"].size()) \ + .front() as int + + if number_of_args == 0: + await input_signal + resolve.call(null) + else: + # only one arg in signal is allowed for now + var result = await input_signal + resolve.call(result) + ) + + +static func from_many(input_signals: Array[Signal]) -> Array[Promise]: + return input_signals.map( + func(input_signal: Signal): + return Promise.from(input_signal) + ) + + +static func all(promises: Array[Promise]) -> Promise: + return Promise.new( + func(resolve: Callable, reject: Callable): + var resolved_promises: Array[bool] = [] + var results := [] + results.resize(promises.size()) + resolved_promises.resize(promises.size()) + resolved_promises.fill(false) + + for i in promises.size(): + promises[i].then( + func(value: Variant): + results[i] = value + resolved_promises[i] = true + if resolved_promises.all(func(value: bool): return value): + resolve.call(results) + ).catch( + func(rejection: Rejection): + reject.call(rejection) + ) + ) + + +static func any(promises: Array[Promise]) -> Promise: + return Promise.new( + func(resolve: Callable, reject: Callable): + var rejected_promises: Array[bool] = [] + var rejections: Array[Rejection] = [] + rejections.resize(promises.size()) + rejected_promises.resize(promises.size()) + rejected_promises.fill(false) + + for i in promises.size(): + promises[i].then( + func(value: Variant): + resolve.call(value) + ).catch( + func(rejection: Rejection): + rejections[i] = rejection + rejected_promises[i] = true + if rejected_promises.all(func(value: bool): return value): + reject.call(PromiseAnyRejection.new(PROMISE_REJECTED, rejections)) + ) + ) + + +class PromiseResult: + var status: Status + var payload: Variant + + func _init(_status: Status, _payload: Variant): + status = _status + payload = _payload + + +class Rejection: + var reason: String + var stack: Array + + func _init(_reason: String): + reason = _reason + stack = get_stack() if OS.is_debug_build() else [] + + + func as_string() -> String: + return ("%s\n" % reason) + "\n".join( + stack.map( + func(dict: Dictionary) -> String: + return "At %s:%i:%s" % [dict["source"], dict["line"], dict["function"]] + )) + + +class PromiseAnyRejection extends Rejection: + var group: Array[Rejection] + + func _init(_reason: String, _group: Array[Rejection]): + super(_reason) + group = _group diff --git a/src/globals/home_adapters.gd b/src/globals/home_adapters.gd index 3717b94..3b54b51 100644 --- a/src/globals/home_adapters.gd +++ b/src/globals/home_adapters.gd @@ -3,4 +3,24 @@ extends Node var Adapter = preload("res://src/home_adapters/adapter.gd") var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS) - \ No newline at end of file +var adapter_ws = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS) + +func _ready(): + add_child(adapter_ws) + + var timer = Timer.new() + timer.set_wait_time(1) + timer.set_one_shot(true) + + print("timer started") + timer.timeout.connect(func(): + print("timer done") + + var result = await adapter_ws.get_state("light.living_room") + print(result) + ) + + add_child(timer) + timer.start() + + diff --git a/src/home_adapters/adapter.gd b/src/home_adapters/adapter.gd index 21c862d..e6de48a 100644 --- a/src/home_adapters/adapter.gd +++ b/src/home_adapters/adapter.gd @@ -1,13 +1,16 @@ extends Node const hass = preload("res://src/home_adapters/hass/hass.gd") +const hass_ws = preload("res://src/home_adapters/hass_ws/hass.gd") enum ADAPTER_TYPES { - HASS + HASS, + HASS_WS } const adapters = { - ADAPTER_TYPES.HASS: hass + ADAPTER_TYPES.HASS: hass, + ADAPTER_TYPES.HASS_WS: hass_ws } const methods = [ @@ -20,6 +23,7 @@ var adapter: Node func _init(type: ADAPTER_TYPES): adapter = adapters[type].new() + add_child(adapter) for method in methods: assert(adapter.has_method(method), "Adapter does not implement method: " + method) @@ -31,4 +35,7 @@ func get_state(entity: String): return await adapter.get_state(entity) func set_state(entity: String, state: String, attributes: Dictionary = {}): - return await adapter.set_state(entity, state, attributes) \ No newline at end of file + return await adapter.set_state(entity, state, attributes) + +func watch_state(entity: String, callback: Callable): + return adapter.watch_state(entity, callback) diff --git a/src/home_adapters/hass/hass.gd b/src/home_adapters/hass/hass.gd index 93d2aa1..da4f49e 100644 --- a/src/home_adapters/hass/hass.gd +++ b/src/home_adapters/hass/hass.gd @@ -19,7 +19,6 @@ func load_devices(): var data_string = response[3].get_string_from_utf8().replace("'", "\"") var json = JSON.parse_string(data_string).data - print(json) return json func get_state(entity: String): @@ -31,7 +30,6 @@ func get_state(entity: String): var data_string = response[3].get_string_from_utf8().replace("'", "\"") var json = JSON.parse_string(data_string) - print(json) return json @@ -58,6 +56,5 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}): var data_string = response[3].get_string_from_utf8().replace("'", "\"") var json = JSON.parse_string(data_string) - print(json) return json diff --git a/src/home_adapters/hass_ws/hass.gd b/src/home_adapters/hass_ws/hass.gd new file mode 100644 index 0000000..b54e920 --- /dev/null +++ b/src/home_adapters/hass_ws/hass.gd @@ -0,0 +1,127 @@ +extends Node + +var devices_template = FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2") +var socket = WebSocketPeer.new() +# in seconds +var request_timeout: float = 10 + +var url: String = "ws://192.168.33.33:8123/api/websocket" +var token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc" + + +var authenticated: bool = false +var id = 1 + +signal packet_received(packet: Dictionary) + +func _init(url := self.url, token := self.token): + self.url = url + self.token = token + + devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"") + connect_ws() + +func connect_ws(): + print("Connecting to %s" % self.url) + socket.connect_to_url(self.url) + +func _process(delta): + socket.poll() + + var state = socket.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + while socket.get_available_packet_count(): + handle_packet(socket.get_packet()) + elif state == WebSocketPeer.STATE_CLOSING: + pass + elif state == WebSocketPeer.STATE_CLOSED: + var code = socket.get_close_code() + var reason = socket.get_close_reason() + print("WS connection closed with code: %s, reason: %s" % [code, reason]) + authenticated = false + set_process(false) + +func handle_packet(raw_packet: PackedByteArray): + var packet = decode_packet(raw_packet) + + print("Received packet: %s" % packet) + + if packet.type == "auth_required": + send_packet({ + "type": "auth", + "access_token": self.token + }) + + elif packet.type == "auth_ok": + authenticated = true + elif packet.type == "auth_invalid": + authenticated = false + print("Authentication failed") + set_process(false) + else: + packet_received.emit(packet) + + +func send_request_packet(packet: Dictionary): + packet.id = id + id += 1 + + send_packet(packet) + + var promise = Promise.new(func(resolve: Callable, reject: Callable): + var handle_packet = func(recieved_packet: Dictionary): + print("Received packet in handler: %s" % recieved_packet) + if packet.id == recieved_packet.id: + print("same id") + resolve.call(recieved_packet) + packet_received.disconnect(handle_packet) + + packet_received.connect(handle_packet) + + var timeout = Timer.new() + timeout.set_wait_time(request_timeout) + timeout.set_one_shot(true) + timeout.timeout.connect(func(): + reject.call(Promise.Rejection.new("Request timed out")) + packet_received.disconnect(handle_packet) + ) + add_child(timeout) + timeout.start() + ) + + return await promise.settled + + +func send_packet(packet: Dictionary): + print("Sending packet: %s" % encode_packet(packet)) + socket.send_text(encode_packet(packet)) + +func decode_packet(packet: PackedByteArray): + return JSON.parse_string(packet.get_string_from_utf8()) + +func encode_packet(packet: Dictionary): + return JSON.stringify(packet) + +func load_devices(): + pass + +func get_state(entity: String): + assert(authenticated, "Not authenticated") + + var result = await send_request_packet({ + "type": "get_states" + }) + + if result.status == Promise.Status.RESOLVED: + return result.payload + return null + + +func watch_state(entity: String, callback: Callable): + assert(authenticated, "Not authenticated") + + +func set_state(entity: String, state: String, attributes: Dictionary = {}): + assert(authenticated, "Not authenticated") + + diff --git a/src/home_adapters/hass_ws/templates/devices.j2 b/src/home_adapters/hass_ws/templates/devices.j2 new file mode 100644 index 0000000..d1750ba --- /dev/null +++ b/src/home_adapters/hass_ws/templates/devices.j2 @@ -0,0 +1,12 @@ +{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None) | list %} + +{%- set ns = namespace(devices = []) %} +{%- for device in devices %} + {%- set entities = device_entities(device) | list %} + {%- if entities %} + {%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %} + {%- endif %} +{%- endfor %} +{ + "data": {{ ns.devices }} +} \ No newline at end of file