elitelighting/elitelights.py
Innovation Science ac79d31ce0
Update to v0.1.1
Fixed log scanning and made ReloadFlags() actually convert the flag bits to a string
2022-05-26 20:05:19 -05:00

391 lines
14 KiB
Python

# A standalone script to control lights according to events in Elite: Dangerous.
# Coded in two afternoons with lots of coffee by Katie.
# This is meant to be a personal project, so I didn't really bother with expandability and such.
# Perhaps I'll revisit this in the future and make it so it can be expended upon easier. Don't count on it.
#
# (C) Innovation Science 2022
try:
import tinytuya
except ModuleNotFoundError:
print("You MUST install tinytuya first.")
print("pip install tinytuya OR python -m pip install tinytuya")
exit()
import time
import random
import json
import os
Version = "0.1.1"
# Use the classes below to configure your lights.
# You MUST go through the following setup process: https://github.com/jasonacox/tinytuya#setup-wizard---getting-local-keys
class PortLight:
# Actual light config.
DeviceID: str = "DEVICEID HERE"
IP: str = "BULB.IP.ADDRESS.HERE"
LocalID: str = "LOCALID HERE"
# Color config. I've left it as my own preferences, but commented out the defaults as well.
red: int = 230
green: int = 100
blue: int = 235
class StarboardLight:
DeviceID: str = "DEVICEID HERE"
IP: str = "BULB.IP.ADDRESS.HERE"
LocalID: str = "LOCALID HERE"
red: int = 230
green: int = 100
blue: int = 235
class HazardLight:
DeviceID: str = "DEVICEID HERE"
IP: str = "BULB.IP.ADDRESS.HERE"
LocalID: str = "LOCALID HERE"
red: int = 255
green: int = 0
blue: int = 0
# Below are the colors for the default hud colors. I'm estimating again, but it's close enough.
# red: int = 255
# green: int = 100
# blue: int = 0
# Elite Dangerous flags
#ED_Docked = 0x0000000000000001
#ED_Landed = 0x0000000000000002
#ED_LandingGearDown = 0x0000000000000004
#ED_ShieldsUp = 0x0000000000000008
#ED_Supercruise = 0x0000000000000010
#ED_FlightAssistOff = 0x0000000000000020
#ED_HardpointsDeployed = 0x0000000000000040
#ED_InWing = 0x0000000000000080
#ED_LightsOn = 0x0000000000000100
#ED_CargoScoopDeployed = 0x0000000000000200
#ED_SilentRunning = 0x0000000000000400
#ED_ScoopingFuel = 0x0000000000000800
#ED_SRVHandbrake = 0x0000000000001000
#ED_SRVTurret = 0x0000000000002000
#ED_SRVTurretRetracted = 0x0000000000004000
#ED_SRVDriveAssist = 0x0000000000008000
#ED_FSDMassLocked = 0x0000000000010000
#ED_FSDCharging = 0x0000000000020000
#ED_FSDCooldown = 0x0000000000040000
#ED_LowFuel = 0x0000000000080000
#ED_OverHeating = 0x0000000000100000
#ED_HasLatLong = 0x0000000000200000
#ED_IsInDanger = 0x0000000000400000
#ED_BeingInterdicted = 0x0000000000800000
#ED_InMainShip = 0x0000000001000000
#ED_InFighter = 0x0000000002000000
#ED_InSRV = 0x0000000004000000
#ED_HudInAnalysisMode = 0x0000000008000000
#ED_NightVision = 0x0000000010000000
# Status file stuff
# StatusDir will usually be: "C:\\Users\\{YOUR USERNAME HERE}\\Saved Games\\Frontier Developments\\Elite Dangerous"
StatusDir = "C:\\Users\\Sam\\Saved Games\\Frontier Developments\\Elite Dangerous"
CurrentLog = ""
StatusJSON = ""
StatusFlags = 0x00000000
StatusFlagsStr = ""
# Makes the infinate loop... loop.
ContinueFlag = True
# States
DoFlicker = False
DoHazards = False
LightsOn = True
LightsDim = False
LastHullHealth = 0
CurrentHullHealth = 0
# Initialize light variables and set version
port = tinytuya.BulbDevice(PortLight.DeviceID, PortLight.IP, PortLight.LocalID, dev_type="default")
starboard = tinytuya.BulbDevice(StarboardLight.DeviceID, StarboardLight.IP, StarboardLight.LocalID, dev_type="default")
hazards = tinytuya.BulbDevice(HazardLight.DeviceID, HazardLight.IP, HazardLight.LocalID, dev_type="default")
# Set Tuya versions
port.set_version(3.3)
starboard.set_version(3.3)
hazards.set_version(3.3)
def SetupLights(): # Initializes lights
# Set light modes to color
port.set_mode(mode='colour')
starboard.set_mode(mode='colour')
hazards.set_mode(mode='colour')
# Turn on the lights
port.turn_on()
starboard.turn_on()
hazards.turn_on()
# Set light colors
port.set_colour(PortLight.red, PortLight.green, PortLight.blue)
starboard.set_colour(StarboardLight.red, StarboardLight.green, StarboardLight.blue)
hazards.set_colour(HazardLight.red, HazardLight.green, HazardLight.blue)
# Set light brightness (port and starboard are dim, whereas hazard is bright.)
port.set_brightness_percentage(brightness=1)
starboard.set_brightness_percentage(brightness=1)
hazards.set_brightness_percentage(brightness=100)
# Turn off hazard.
hazards.turn_off()
def NormalLights(): # Returns lights to normal operation (used when exiting)
# Change lights back to white mode.
port.set_mode(mode='white')
starboard.set_mode(mode='white')
hazards.set_mode(mode='white')
# Set brightness to full to blind the user.
port.set_brightness_percentage(brightness=100)
starboard.set_brightness_percentage(brightness=100)
hazards.set_brightness_percentage(brightness=100)
def FlickerLights():
# So far, I've made it so there are four types of flickers possible (0 being no flicker.)
choices = [0, 1, 2, 3, 4]
choice = random.choice(choices) # Randomly choose a flicker type
flickertime = random.random() # Randomly choose a flicker time (0 to 1 sec)
# Welcome to if-else hell! I'm using python 3.9.6 so no pattern matching for me, and I don't care enough to deal with dictionaries.
# If you would like to add more flicker patterns, add them here and add a new number to the choices list.
if(choice==0):
# Nothing happens.
pass
elif(choice==1):
# Flicker port
port.turn_off()
time.sleep(flickertime)
port.turn_on()
elif(choice==2):
# Flicker starboard
starboard.turn_off()
time.sleep(flickertime)
starboard.turn_on()
elif(choice==3):
# Flicker both, port first
port.turn_off()
starboard.turn_off()
time.sleep(flickertime)
port.turn_on()
starboard.turn_on()
elif(choice==4):
starboard.turn_off()
port.turn_off()
time.sleep(flickertime)
starboard.turn_on()
port.turn_on()
else:
print("How did we get here?")
def MainLightsOn(): # Turns main (port and starboard) lights on
port.turn_on()
starboard.turn_on()
def MainLightsOff(): # Turns main (port and starboard) lights off
port.turn_off()
starboard.turn_off()
def DimAllLights(): # Dims all lights. Used when FSD is charging to make it look like it's heavily loading the powerplant.
port.set_brightness_percentage(brightness=0)
starboard.set_brightness_percentage(brightness=0)
hazards.set_brightness_percentage(brightness=80)
def LightsStandard(): # Standard brightness configuration for lights.
port.set_brightness_percentage(brightness=1)
starboard.set_brightness_percentage(brightness=1)
hazards.set_brightness_percentage(brightness=100)
def HazardsOn(): # Turns the hazard light on
hazards.turn_on()
def HazardsOff(): # Turns the hazard light off
hazards.turn_off()
def ReloadFlags(): # Reloads Elite: Dangerous status flags and sets flicker, hazard on/off, lights on/off, and dimming variables for later processing
global StatusDir
global StatusJSON
global StatusFlags
global StatusFlagsStr
global DoFlicker
global DoHazards
global LightsOn
global LightsDim
global ContinueFlag
# Open the status file
f = open(StatusDir + "\\Status.json", "r")
try:
StatusJSON = json.loads(f.read()) # Read the json from the Status.json file.
except:
#print("JSONDecodeError in ReloadFlags(). Continuing.")
pass # Sometimes it guffs up reading (JSONDecodeError), and I can't really fix this.
else: # This makes the logic below use the previous status flags if an exception is raised
StatusFlags = "0x%8x" % StatusJSON["Flags"] # Extrapolate the flags from the json file and make it a known length (8 hex digits)
f.close() # Close Status.json
try:
StatusFlagsStr = str(StatusFlags)
# Convert the ship state (3rd from right) byte (LightsOn, Cargo Scoop Deployed, Silent Running, Scooping Fuel)
ShipStateByte = bin(int(StatusFlagsStr[-3]))[2:].zfill(4)
ShipStateByteStr = str(ShipStateByte)
# Convert the FSD (5th from right) byte (FSD MassLocked, FSD Charging, FSD Cooldown, Low Fuel (<25%))
FSDByte = bin(int(StatusFlagsStr[-5]))[2:].zfill(4)
FSDByteStr = str(FSDByte)
# Convert the hazards (6th from right) byte (Overheating (>100%), Has Lat Long, IsInDanger, Being Interdicted)
HazardByte = bin(int(StatusFlagsStr[-6]))[2:].zfill(4)
HazardByteStr = str(HazardByte)
# If you would like to add more effects to the lights, add them here.
if(HazardByteStr[-1] == "1" or LastHullHealth > CurrentHullHealth):
DoFlicker = True # Overheating and taking damage causes lights to flicker
else:
DoFlicker = False
if(int(StatusFlagsStr[-6]) > 0): # If there's any hazard, hazard light is turned on
DoHazards = True
else:
DoHazards = False
if(ShipStateByteStr[-3] == "1"): # Port and starboard lights turn off when rigged for silent running
LightsOn = False
else:
LightsOn = True
if(FSDByteStr[-2] == "1"): # Lights dim when the FSD is charging
LightsDim = True
else:
LightsDim = False
except ValueError:
print("ValueError! More than likely, Elite: Dangerous is closed. Exiting.")
ContinueFlag = False
#except IndexError:
# print("IndexError encountered in ReloadFlags. This may be recoverable. Continuing.")
def GetCurrentLog(): # Gets the most recent log file left by Elite: Dangerous.
global StatusDir
LogName = ""
# Changes working dir to the saved games dir temporarily, remembering the original working dir
OriginalDir = os.getcwd()
os.chdir(StatusDir)
StatusDirContents = sorted(os.listdir(StatusDir), key=os.path.getmtime) # Get and sort the "saved game" directory from newest first.
os.chdir(OriginalDir)
for f in reversed(StatusDirContents):
if(f[-4:] == ".log"): # Search for the newest .log file.
LogName = f # We found the file!
break
print("Discovered current log file: " + f)
return LogName # Return the newest log file.
def GetDamageStats(): # Extrapolates damage taken from the log file. I'm not sure if this works quite yet.
global StatusDir
global CurrentLog
try:
f = open(StatusDir + "\\" + CurrentLog, "r") # Open the log file
except:
print("Error opening log. Continuing.") # Error checking for if the log file cannot be opened.
return
try:
Lines = f.readlines() # This should never happen.
except:
print("Error reading lines from log. Continuing")
f.close() # Close the log file
i = 0
tempjson = ""
for line in reversed(Lines): # Scan in reversed for the newest loadout readings
i += 1
try:
tempjson = json.loads(line) # Attempt to load the line as JSON.
except:
continue # I'm willing to bet that we will encounter a similar JSONDecodeError here on occasion, so we skip that line.
# This will probably lead to issues, but I'm not too worried about it.
if(tempjson["event"] == "Loadout"):
return tempjson["HullHealth"] # Returns HullHealth from Loadout event.
def RunStates(): # Processes light effect variables set by ReloadFlags()
if(DoFlicker):
FlickerLights()
if(DoHazards):
HazardsOn()
else:
HazardsOff()
if(LightsOn):
MainLightsOn()
else:
MainLightsOff()
if(LightsDim):
DimAllLights()
else:
LightsStandard()
# Program initialization
print("Elite Lighting v. " + Version)
print("If the program crashes in this stage, you either need to open Elite: Dangerous first or configure the program.")
print("To configure the program, follow the instructions at: https://git.innovation-inc.org/Innovation/elitelighting")
print("You will also need to read alongside: https://github.com/jasonacox/tinytuya#setup-wizard---getting-local-keys")
print("Failure to do so will cause a crash and may kill your cat(s).")
print("\n")
print("Setting up lights")
SetupLights() # Set up lights
print("Lights set up.")
print("Getting current Elite: Dangerous log")
CurrentLog = GetCurrentLog() # Get current Elite: Dangerous log
print("Ready. Press CTRL+C at any time to set lights back to normal operation.")
while ContinueFlag == True:
try:
LastHullHealth = CurrentHullHealth
CurrentHullHealth = GetDamageStats()
ReloadFlags()
RunStates()
time.sleep(0.25)
except KeyboardInterrupt: # This makes it so CTRL+C begins the program exiting sequence, and so the program exits gracefully at any point during execution.
break
HazardsOn() # Turn on hazards so that it isn't off when lights return to normal operation.
lightschoice = input("Would you like your lights off? Y/n > ") # Would you like to be light mode'd?
if(lightschoice != "n" and lightschoice != "N"):
print("Turning off lights.") # No, I like having eyes.
MainLightsOff()
HazardsOff()
print("Returning lights to normal operation.")
NormalLights() # Return lights to normal operation (white mode, full brightness)
print("Goodbye.")