start implementing ws support

This commit is contained in:
Nitwel 2023-11-02 23:13:55 +01:00
parent de976f34bc
commit 40adc7b8ef
9 changed files with 359 additions and 11 deletions

View File

@ -3,9 +3,9 @@ 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")
@export var passthrough: bool = true
@onready var environment: WorldEnvironment = $WorldEnvironment @onready var environment: WorldEnvironment = $WorldEnvironment
func _ready(): func _ready():
if passthrough: # In case we're running on the headset, use the passthrough sky
environment.environment.sky.set_material(sky_passthrough) if OS.get_name() == "Android":
environment.environment.sky.set_material(sky_passthrough)

View File

@ -28,7 +28,6 @@ ambient_light_sky_contribution = 0.72
[node name="Main" type="Node3D"] [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) 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") script = ExtResource("1_uvrd4")
passthrough = false
[node name="XROrigin3D" type="XROrigin3D" parent="."] [node name="XROrigin3D" type="XROrigin3D" parent="."]

21
promise/LICENSE Normal file
View File

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

165
promise/promise.gd Normal file
View File

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

View File

@ -3,4 +3,24 @@ extends Node
var Adapter = preload("res://src/home_adapters/adapter.gd") var Adapter = preload("res://src/home_adapters/adapter.gd")
var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS) var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS)
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()

View File

@ -1,13 +1,16 @@
extends Node extends Node
const hass = preload("res://src/home_adapters/hass/hass.gd") 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 { enum ADAPTER_TYPES {
HASS HASS,
HASS_WS
} }
const adapters = { const adapters = {
ADAPTER_TYPES.HASS: hass ADAPTER_TYPES.HASS: hass,
ADAPTER_TYPES.HASS_WS: hass_ws
} }
const methods = [ const methods = [
@ -20,6 +23,7 @@ var adapter: Node
func _init(type: ADAPTER_TYPES): func _init(type: ADAPTER_TYPES):
adapter = adapters[type].new() adapter = adapters[type].new()
add_child(adapter)
for method in methods: for method in methods:
assert(adapter.has_method(method), "Adapter does not implement method: " + method) 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) return await adapter.get_state(entity)
func set_state(entity: String, state: String, attributes: Dictionary = {}): func set_state(entity: String, state: String, attributes: Dictionary = {}):
return await adapter.set_state(entity, state, attributes) return await adapter.set_state(entity, state, attributes)
func watch_state(entity: String, callback: Callable):
return adapter.watch_state(entity, callback)

View File

@ -19,7 +19,6 @@ func load_devices():
var data_string = response[3].get_string_from_utf8().replace("'", "\"") var data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string).data var json = JSON.parse_string(data_string).data
print(json)
return json return json
func get_state(entity: String): 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 data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string) var json = JSON.parse_string(data_string)
print(json)
return 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 data_string = response[3].get_string_from_utf8().replace("'", "\"")
var json = JSON.parse_string(data_string) var json = JSON.parse_string(data_string)
print(json)
return json return json

View File

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

View File

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