Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Room Media API Calls #21

Merged
merged 5 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pyControl4/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class C4Entity:
def __init__(self, C4Director, item_id):
"""Creates a Control4 object.

Parameters:
`C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the device is connected to.

`item_id` - The Control4 item ID.
"""
self.director = C4Director
self.item_id = int(item_id)
14 changes: 2 additions & 12 deletions pyControl4/alarm.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
"""Controls Control4 security panel and contact sensor (door, window, motion) devices.
"""
import json
from pyControl4 import C4Entity


class C4SecurityPanel:
def __init__(self, C4Director, item_id):
"""Creates a Control4 Security Panel object.

Parameters:
`C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the security panel is connected to.

`item_id` - The Control4 item ID of the security panel partition.
"""
self.director = C4Director
self.item_id = item_id

class C4SecurityPanel(C4Entity):
async def getArmState(self):
"""Returns the arm state of the security panel as "DISARMED", "ARMED_HOME", or "ARMED_AWAY"."""
disarmed = await self.director.getItemVariableValue(
Expand Down
18 changes: 6 additions & 12 deletions pyControl4/blind.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@
"""


class C4Blind:
def __init__(self, C4Director, item_id):
"""Creates a Control4 Blind object.
from pyControl4 import C4Entity

Parameters:
`C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the blind is connected to.

`item_id` - The Control4 item ID of the blind.
"""
self.director = C4Director
self.item_id = item_id

class C4Blind(C4Entity):
async def getBatteryLevel(self):
"""Returns the battery of a blind. We currently don't know the range or meaning."""
value = await self.director.getItemVariableValue(self.item_id, "Battery Level")
return int(value)

async def getClosing(self):
"""Returns an indication of whether the blind is moving in the closed direction as a boolean
(True=closing, False=opening). If the blind is stopped, reports the direction it last moved."""
(True=closing, False=opening). If the blind is stopped, reports the direction it last moved.
"""
value = await self.director.getItemVariableValue(self.item_id, "Closing")
return bool(value)

Expand Down Expand Up @@ -52,7 +45,8 @@ async def getOpen(self):

async def getOpening(self):
"""Returns an indication of whether the blind is moving in the open direction as a boolean
(True=opening, False=closing). If the blind is stopped, reports the direction it last moved."""
(True=opening, False=closing). If the blind is stopped, reports the direction it last moved.
"""
value = await self.director.getItemVariableValue(self.item_id, "Opening")
return bool(value)

Expand Down
95 changes: 93 additions & 2 deletions pyControl4/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,12 @@ async def getItemVariableValue(self, item_id, var_name):
Parameters:
`item_id` - The Control4 item ID.

`var_name` - The Control4 variable name.
`var_name` - The Control4 variable name or names.
"""

if isinstance(var_name, (tuple, list, set)):
var_name = ",".join(var_name)

data = await self.sendGetRequest(
"/api/v1/items/{}/variables?varnames={}".format(item_id, var_name)
)
Expand All @@ -171,8 +175,11 @@ async def getAllItemVariableValue(self, var_name):
for all items that have it.

Parameters:
`var_name` - The Control4 variable name.
`var_name` - The Control4 variable name or names.
"""
if isinstance(var_name, (tuple, list, set)):
var_name = ",".join(var_name)

data = await self.sendGetRequest(
"/api/v1/items/variables?varnames={}".format(var_name)
)
Expand Down Expand Up @@ -209,3 +216,87 @@ async def getItemBindings(self, item_id):
`item_id` - The Control4 item ID.
"""
return await self.sendGetRequest("/api/v1/items/{}/bindings".format(item_id))

async def getUiConfiguration(self):
"""Returns a dictionary of the JSON Control4 App UI Configuration enumerating rooms and capabilities

Returns:

{
"experiences": [
{
"type": "watch",
"sources": {
"source": [
{
"id": 59,
"type": "HDMI"
},
{
"id": 946,
"type": "HDMI"
},
{
"id": 950,
"type": "HDMI"
},
{
"id": 33,
"type": "VIDEO_SELECTION"
}
]
},
"active": false,
"room_id": 9,
"username": "primaryuser"
},
{
"type": "listen",
"sources": {
"source": [
{
"id": 298,
"type": "DIGITAL_AUDIO_SERVER",
"name": "My Music"
},
{
"id": 302,
"type": "AUDIO_SELECTION",
"name": "Stations"
},
{
"id": 306,
"type": "DIGITAL_AUDIO_SERVER",
"name": "ShairBridge"
},
{
"id": 937,
"type": "DIGITAL_AUDIO_SERVER",
"name": "Spotify Connect"
},
{
"id": 100002,
"type": "DIGITAL_AUDIO_CLIENT",
"name": "Digital Media"
}
]
},
"active": false,
"room_id": 9,
"username": "primaryuser"
},
{
"type": "cameras",
"sources": {
"source": [
{
"id": 877,
"type": "Camera"
},
...
}
...
}
"""

return await self.sendGetRequest("/api/v1/agents/ui_configuration")
12 changes: 2 additions & 10 deletions pyControl4/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@
"""


class C4Light:
def __init__(self, C4Director, item_id):
"""Creates a Control4 Light object.
from pyControl4 import C4Entity

Parameters:
`C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the light is connected to.

`item_id` - The Control4 item ID of the light.
"""
self.director = C4Director
self.item_id = item_id

class C4Light(C4Entity):
async def getLevel(self):
"""Returns the level of a dimming-capable light as an int 0-100.
Will cause an error if called on a non-dimmer switch. Use `getState()` instead.
Expand Down
13 changes: 2 additions & 11 deletions pyControl4/relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,10 @@
"""


class C4Relay:
def __init__(self, C4Director, item_id):
"""Creates a Control4 Relay object.
from pyControl4 import C4Entity

Parameters:
`C4Director` - A `pyControl4.director.C4Director` object that
corresponds to the Control4 Director that the Relay is connected to.

`item_id` - The Control4 item ID of the Relay.
"""
self.director = C4Director
self.item_id = item_id

class C4Relay(C4Entity):
async def getRelayState(self):
"""Returns the current state of the relay.

Expand Down
154 changes: 154 additions & 0 deletions pyControl4/room.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Controls Control4 Room devices.
"""


from pyControl4 import C4Entity


class C4Room(C4Entity):
"""
A media-oriented view of a Control4 Room, supporting items of type="room"
"""

async def isRoomHidden(self) -> bool:
"""Returns True if the room is hidden from the end-user"""
value = await self.director.getItemVariableValue(self.item_id, "ROOM_HIDDEN")
return int(value) != 0

async def isOn(self) -> bool:
"""Returns True/False if the room is "ON" from the director's perspective"""
value = await self.director.getItemVariableValue(self.item_id, "POWER_STATE")
return int(value) != 0

async def setRoomOff(self):
"""Turn the room "OFF" """
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"ROOM_OFF",
{},
)

async def _setSource(self, source_id: int, audio_only: bool):
"""
Sets the room source, turning on the room if necessary.
If audio_only, only the current audio device is changed
"""
await self.director.sendPostRequest(
f"/api/v1/items/{self.item_id}/commands",
"SELECT_AUDIO_DEVICE" if audio_only else "SELECT_VIDEO_DEVICE",
{"deviceid": source_id},
)

async def setAudioSource(self, source_id: int):
"""Sets the current audio source for the room"""
await self._setSource(source_id, audio_only=True)

async def setVideoAndAudioSource(self, source_id: int):
"""Sets the current audio and video source for the room"""
await self._setSource(source_id, audio_only=False)

async def getVolume(self) -> int:
"""Returns the current volume for the room from 0-100"""
value = await self.director.getItemVariableValue(self.item_id, "CURRENT_VOLUME")
return int(value)

async def isMuted(self) -> bool:
"""Returns True if the room is muted"""
value = await self.director.getItemVariableValue(self.item_id, "IS_MUTED")
return int(value) != 0

async def setMuteOn(self):
"""Mute the room"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"MUTE_ON",
{},
)

async def setMuteOff(self):
"""Unmute the room"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"MUTE_OFF",
{},
)

async def toggleMute(self):
"""Toggle the current mute state for the room"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"MUTE_TOGGLE",
{},
)

async def setVolume(self, volume: int):
"""Set the room volume, 0-100"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"SET_VOLUME_LEVEL",
{"LEVEL": volume},
)

async def setIncrementVolume(self):
"""Decrease volume by 1"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"PULSE_VOL_UP",
{},
)

async def setDecrementVolume(self):
"""Decrease volume by 1"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"PULSE_VOL_DOWN",
{},
)

async def setPlay(self):
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"PLAY",
{},
)

async def setPause(self):
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"PAUSE",
{},
)

async def setStop(self):
"""Stops the currently playing media but does not turn off the room"""
await self.director.sendPostRequest(
"/api/v1/items/{}/commands".format(self.item_id),
"STOP",
{},
)

async def getAudioDevices(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you document/provide an example of what kind of data format is returned by this call?

See here for an example:

async def getAccountControllers(self):

Copy link
Contributor Author

@nalin29 nalin29 Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API call on OS 3.2.3, just returns None for me on both getAudioDevices/getVideoDevices. I do not make use of the functions in the HA integration instead we use the UI. Perhaps we delete these API Calls since they are not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

Copy link
Owner

@lawtancool lawtancool Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns None? Like literal Python None or some sort of valid JSON response that just says no devices?

I'm inclined to leave it in with a comment that says it returns none on OS 3.2.3 if it's actually a valid JSON response from Control4. Perhaps other configurations/older OS versions make use of this call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah its just None the python literal. That is why I labeled as an invalid API call and deleted but I can put it back with comment that it wasn't valid in OS 3.2.3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I committed a change that brings back the functions and adds a warning note.

"""
Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

Get the audio devices located in the room.
Note that this is literally the devices in the room,
not necessarily all devices _playable_ in the room.
See C4Director.getUiConfiguration for a more accurate list
"""
await self.director.sendGetRequest(
"/api/v1/locations/rooms/{}/audio_devices".format(self.item_id)
)

async def getVideoDevices(self):
"""
Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

Get the video devices located in the room.
Note that this is literally the devices in the room,
not necessarily all devices _playable_ in the room.
See C4Director.getUiConfiguration for a more accurate list
"""
await self.director.sendGetRequest(
"/api/v1/locations/rooms/{}/video_devices".format(self.item_id)
)
3 changes: 2 additions & 1 deletion pyControl4/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def __init__(
def item_callbacks(self):
"""Returns a dictionary of registered item ids (key) and their callbacks (value).

item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead."""
item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.
"""
return self._item_callbacks

def add_item_callback(self, item_id, callback):
Expand Down