I wrote a Python script to configure Crestron AirMedia devices, via the REST api interface on each device. The intention for this script is to use the command line to configure many dozens of AirMedia devices at once, by typing “python AirMediaAPI.py” (or, use the filename you save the python as).

The script imports a .csv file that contains a line entry for each AirMedia to be configured. The .csv file has the following fields:

  • target_ip (current ip address of the AirMedia to be configured)
  • room_name (name of the intended room – the script will also turn this into the device’s Hostname)
  • intended_dns (new static DNS)
  • vlan_ip (new static ip address for main PoE NIC)
  • vlan_subnet (new subnet mask for main PoE NIC)
  • vlan_gateway (new default gateway for main PoE NIC)
  • is_conference (flag indicating which settings the script should configure)
  • user (admin username of AirMedia – script will create a user with this username, if needed)
  • passwd (admin password of AirMedia – script will use to create user, if needed)

Github repo

No public repo, right now.

Code

# Crestron AirMedia API Config 0.9.4
# Required libraries: os, requests, urllib3, pandas (all are installed with native python, but you still need import into script)
import os
import requests
import urllib3
import pandas as pd
# Disable error message for unsecure connection
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Get username from system and create filepath variables
user = os.environ.get('USER', os.environ.get('USERNAME'))
data_filepath = f"C:/Users/{user}/Downloads/am_api_template.csv"
puf_filepath = f"C:/Users/{user}/Downloads/am3xxx_3.0700.0379.puf" # I just update this, as needed
# Read am_api_template.csv into python as a list of key-value pairs
d = pd.read_csv(data_filepath, header=0)
data = []
for row in d.index:
i = {}
for col in d.columns:
i[col] = d.loc[row,col]
data.append(i)
del(d)
# Config - loops through each entry in data variable's key-value pairs and configures the parameters listed in "iterator" variable
for am in data:
target_ip = am["target_ip"]
index_am = "https://" + target_ip + "/index_airmedia.html"
index_banner = "https://" + target_ip + "/index_banner.html"
url_create_user = "https://" + target_ip + "/createUser.html"
url_login = "https://" + target_ip + "/userlogin.html"
room_name = am["room_name"]
am_name = (room_name + "-AM3200WF").upper().replace(" ", "").strip()
intended_dns = am["intended_dns"]
is_room_conference = am["is_conference"]
intended_ip = am["vlan_ip"]
intended_subnet = am["vlan_subnet"]
intended_gateway = am["vlan_gateway"]
am_user = am['user']
am_passwd = am['passwd']
# iterator is a list of tuples. each tuple consists of (0) a directory entry and (1) a json object with the data for that entry
if is_room_conference == 0:
iterator = [
("/Device/NetworkAdapters/HostName", {"Device": {"NetworkAdapters": {"HostName": am_name}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowConnectionInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowConnectionInfo": False}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowConnectionInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"PersistConnectionInfo": False}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowAirplayInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowAirplayInfo": False}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowMiracastInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowMiracastInfo": False}}}}),
("/Device/Bluetooth/IsEnabled", {"Device": {"Bluetooth": {"IsEnabled": False}}}),
("/Device/App/Config/General/RoomName", {"Device": {"App": {"Config": {"General": {"RoomName": room_name}}}}}),
("/Device/App/Config/RuntimeSettings/ShowOverlay", {"Device": {"App": {"Config": {"RuntimeSettings": {"ShowOverlay": False}}}}}),
("/Device/App/Config/RuntimeSettings/PinPointUXFontSize", {"Device": {"App": {"Config": {"RuntimeSettings": {"PinPointUXFontSize": "Large"}}}}}),
("/Device/NetworkAdapters/DnsSettings/IPv4/StaticDns", {"Device": {"NetworkAdapters": {"DnsSettings": {"IPv4": {"StaticDns": [intended_dns]}}}}}),
("/Device/App/Config/RuntimeSettings/ShowCalendarClockOverlay", {"Device": {"App": {"Config": {"RuntimeSettings": {"ShowCalendarClockOverlay": False}}}}}),
("/Device/IpTableV2", {"Device": {"IpTableV2": {"EntriesCurrentKeyList": ["7"], "Entries": {"7": {"Address": "172.22.0.1", "IpId": "7", "Port": 41794, "ProgramInstanceId": ""}}}}}),
("/Device/SystemClock/Ntp/Server1/Address", {"Device": {"SystemClock": {"Ntp": {"Servers": {"Server1": {"Address": "10.40.8.1"}}}}}}),
("/Device/AirMedia/SystemMode", {"Device": {"AirMedia": {"SystemMode": "OptimizedForVideoQuality"}}}),
("/Device/NetworkAdapters/Adapters", {"Device": {"NetworkAdapters": {"Adapters": {"EthernetLan": {"IPv4": {"StaticAddresses": [{"Address": intended_ip, "SubnetMask": intended_subnet}], "StaticDefaultGateway": intended_gateway, "IsDhcpEnabled": False}}}}}})
]
else:
iterator = [
("/Device/NetworkAdapters/HostName", {"Device": {"NetworkAdapters": {"HostName": am_name}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowConnectionInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowConnectionInfo": True}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowConnectionInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"PersistConnectionInfo": False}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowAirplayInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowAirplayInfo": True}}}}),
("/Device/AirMedia/ConnectionDisplayOptions/ShowMiracastInfo", {"Device": {"AirMedia": {"ConnectionDisplayOptions": {"ShowMiracastInfo": True}}}}),
("/Device/Bluetooth/IsEnabled", {"Device": {"Bluetooth": {"IsEnabled": True}}}),
("/Device/App/Config/General/RoomName", {"Device": {"App": {"Config": {"General": {"RoomName": room_name}}}}}),
("/Device/App/Config/RuntimeSettings/ShowOverlay", {"Device": {"App": {"Config": {"RuntimeSettings": {"ShowOverlay": False}}}}}),
("/Device/App/Config/RuntimeSettings/PinPointUXFontSize", {"Device": {"App": {"Config": {"RuntimeSettings": {"PinPointUXFontSize": "Large"}}}}}),
("/Device/NetworkAdapters/DnsSettings/IPv4/StaticDns", {"Device": {"NetworkAdapters": {"DnsSettings": {"IPv4": {"StaticDns": [intended_dns]}}}}}),
("/Device/App/Config/RuntimeSettings/ShowCalendarClockOverlay", {"Device": {"App": {"Config": {"RuntimeSettings": {"ShowCalendarClockOverlay": False}}}}}),
("/Device/IpTableV2", {"Device": {"IpTableV2": {"EntriesCurrentKeyList": ["7"], "Entries": {"7": {"Address": "172.22.0.1", "IpId": "7", "Port": 41794, "ProgramInstanceId": ""}}}}}),
("/Device/SystemClock/Ntp/Server1/Address", {"Device": {"SystemClock": {"Ntp": {"Servers": {"Server1": {"Address": "10.40.8.1"}}}}}}),
("/Device/AirMedia/SystemMode", {"Device": {"AirMedia": {"SystemMode": "OptimizedForVideoQuality"}}}),
("/Device/NetworkAdapters/Adapters", {"Device": {"NetworkAdapters": {"Adapters": {"EthernetLan": {"IPv4": {"StaticAddresses": [{"Address": intended_ip, "SubnetMask": intended_subnet}], "StaticDefaultGateway": intended_gateway, "IsDhcpEnabled": False}}}}}})
]
# Set first-time admin user, if needed
try:
r = requests.Session()
r.get("https://" + target_ip, verify=False)
init_track_id = requests.utils.dict_from_cookiejar(r.cookies)
auth_headers = {"Cookie": init_track_id['TRACKID'],
"Host": target_ip,
"Referer": "https://" + target_ip + "/"}
r.get(index_am, verify=False, headers=auth_headers)
auth_headers['Referer'] = index_am
r.get(index_banner, verify=False, headers=auth_headers)
auth_headers['Referer'] = index_banner
r.get(url_create_user, verify=False, headers=auth_headers)
auth_headers['Referer'] = url_create_user
auth_headers['Origin'] = "https://" + target_ip
r.post("https://" + target_ip + "/Device/Authentication", verify=False, headers=auth_headers, json={"Device": {"Authentication": {"AuthenticationState": {"AdminPassword": am_passwd, "AdminUsername": am_user, "IsEnabled": True}}}})
r.close()
print(f"created user for {target_ip}")
except:
r.close()
print(f"{target_ip} already had a user")
# Settings Config
s = requests.Session()
s.get(url_login, verify=False)
init_track_id = requests.utils.dict_from_cookiejar(s.cookies)
post_headers = {"Cookie": next(iter(init_track_id)) + "=" + init_track_id['TRACKID'],
"Origin": target_ip,
"Referer": target_ip + "/userlogin.html"}
post_body = f"login={am_user}&&passwd={am_passwd}"
auth = s.post(url_login, verify=False, headers=post_headers, data=post_body)
print("Configuring " + room_name)
get_token = {"CREST-XSRF-TOKEN": auth.headers["CREST-XSRF-TOKEN"]}
post_token = {"X-CREST-XSRF-TOKEN": auth.headers["CREST-XSRF-TOKEN"]}
for i in iterator:
r = s.post("https://" + target_ip + i[0], json = i[1], verify=False, headers=post_token)
s.close()
# Powershell script to handle firmware update and restart. It will trigger a possible firmware upgrade, if the version in your downloads folder is newer than that on your AirMedia. It will not check for completion of the upgrade.
os.system(f"powershell -File C:/Users/{user}/Downloads/AirMediaAPI_Powershell.ps1 -target_ip {target_ip} -filepath {puf_filepath}")
print("Completed " + room_name)
print("All done. Wait for firmware update, if you think an AirMedia updated firmware (this tool passes the update-puf command, but does not check for completion).")

The python script executes a powershell script that covers firmware updates and device reboot. You can do this through the api, but I found powershell to be effective.

import-module pscrestron -usewindowspowershell
# set target_ip and filepath as mandatory parameters when invoking this script
param(
[Parameter(Mandatory=$true)]
[string]$target_ip
)
param(
[Parameter(Mandatory=$true)]
[string]$filepath
)
# Update Firmware
update-puf -device $target_ip -path $filepath -secure -username "example" -password "example" -ShowProgress;
# Reboot
invoke-crestroncommand -device $target_ip -secure -username "example" -password "example" -command "reboot";

Future changes

One change that I might implement for full release, is converting this to a web app, but I haven’t decided yet. I will probably wait until others in my org start using this, and get their feedback.

Python package for interacting with AirMedia API: forthcoming