start implementing ws support
This commit is contained in:
parent
de976f34bc
commit
40adc7b8ef
6
main.gd
6
main.gd
|
@ -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)
|
||||||
|
|
|
@ -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
21
promise/LICENSE
Normal 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
165
promise/promise.gd
Normal 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
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
127
src/home_adapters/hass_ws/hass.gd
Normal file
127
src/home_adapters/hass_ws/hass.gd
Normal 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")
|
||||||
|
|
||||||
|
|
12
src/home_adapters/hass_ws/templates/devices.j2
Normal file
12
src/home_adapters/hass_ws/templates/devices.j2
Normal 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 }}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user