Skip to content
Open
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
94 changes: 83 additions & 11 deletions NeewerLite-Python.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@
]

customLightPresets = defaultLightPresets[:] # copy the default presets to the list for the current session's presets
hardwareMACOverrides = {} # map of BLE GUID/MAC to hardware MAC (used for Infinity lights on macOS)
pendingHWMacOverride = "" # temporary CLI override passed via --hwmac

threadAction = "" # the current action to take from the thread
serverBusy = [False, ""] # whether or not the HTTP server is busy
Expand All @@ -200,6 +202,7 @@
anotherInstance = False # whether or not we're using a new instance (for the Singleton check)
globalPrefsFile = os.path.dirname(os.path.abspath(sys.argv[0])) + os.sep + "light_prefs" + os.sep + "NeewerLite-Python.prefs" # the global preferences file for saving/loading
customLightPresetsFile = os.path.dirname(os.path.abspath(sys.argv[0])) + os.sep + "light_prefs" + os.sep + "customLights.prefs"
hardwareMACOverridesFile = os.path.dirname(os.path.abspath(sys.argv[0])) + os.sep + "light_prefs" + os.sep + "hardwareMACs.prefs"

# FILE LOCKING FOR SINGLE INSTANCE
def singleInstanceLock():
Expand Down Expand Up @@ -2927,6 +2930,27 @@ def loadCustomPresets():
customLightPresets[6] = paramsList
elif "customPreset7=" in customPresets[a]:
customLightPresets[7] = paramsList

def loadHardwareMACOverrides():
global hardwareMACOverrides

if not os.path.exists(hardwareMACOverridesFile):
return

with open(hardwareMACOverridesFile, mode="r", encoding="utf-8") as fileToOpen:
lines = fileToOpen.read().splitlines()

for line in lines:
line = line.strip()
if line == "" or line.startswith("#"):
continue

if "=" in line:
key, value = line.split("=", 1)
key = key.strip().upper()
value = value.strip().upper()
if key != "" and value != "":
hardwareMACOverrides[key] = value

# RETURN THE CORRECT NAME FOR THE IDENTIFIER OF THE LIGHT (FOR DEBUG STRINGS)
def returnMACname():
Expand Down Expand Up @@ -2983,6 +3007,9 @@ def printDebugString(theString):
serverBusy[1] = ""

def splitMACAddress(MACAddress, returnInt = False):
if MACAddress == None:
raise ValueError("Hardware MAC address is missing (Infinity light on macOS). Close other apps and re-scan, or check system_profiler output.")

MACAddress = MACAddress.split(":")

if returnInt == False:
Expand Down Expand Up @@ -3298,9 +3325,9 @@ def updateStatus(splitString = "", infinityMode = 0, customValue = None):

# Use this class to store information in a format that plays nicer with Bleak > 0.19
class UpdatedBLEInformation:
def __init__(self, name, address, rssi, HWMACaddr = None):
def __init__(self, name, address, rssi, HWMACaddr = None, realname = None):
self.name = name # the corrected name of this device (SL90 Pro)
self.realname = name # the real name of this device (NW-2342520000FFF, etc.)
self.realname = realname if realname != None else name # the real name of this device (NW-2342520000FFF, etc.)
self.address = address # the MAC address (or in the case of MacOS, the GUID)
self.rssi = rssi # the signal level of this device
self.HWMACaddr = HWMACaddr # the exact MAC address (needed for MacOS) of this device
Expand Down Expand Up @@ -3335,20 +3362,32 @@ async def findDevices(limitToDevices = None):
for d in devices: # go through all of the devices Bleak just found
if limitToDevices != None: # we're looking for devices using the CLI, so we need *specific* MAC addresses/GUIDs
if d.address in limitToDevices:
raw_name = d.name
d.name = getCorrectedName(d.name)
d.realname = raw_name
if d.address.upper() in hardwareMACOverrides:
d.HWMACaddr = hardwareMACOverrides[d.address.upper()]
currentScan.append(d)
else: # we're doing a normal device discovery/re-discovery scan
if d.address in whiteListedMACs: # if the MAC address is in the list of whitelisted addresses, add this device
printDebugString(f"Matching whitelisted address found - {returnMACname()} {d.address}, adding to the list")
raw_name = d.name
d.name = getCorrectedName(d.name)
d.realname = raw_name
if d.address.upper() in hardwareMACOverrides:
d.HWMACaddr = hardwareMACOverrides[d.address.upper()]
currentScan.append(d)
else: # if this device is not whitelisted, check to see if it's valid (contains "NEEWER" in the name)
if d.name != None:
acceptedPrefixes = ["NEEWER", "NW-", "SL", "NWR"]

for a in range(len(acceptedPrefixes)):
if acceptedPrefixes[a] in d.name:
raw_name = d.name
d.name = getCorrectedName(d.name) # fix the "newer" light names, like NW-20220057 with their correct names, like SL90 Pro
d.realname = raw_name
if d.address.upper() in hardwareMACOverrides:
d.HWMACaddr = hardwareMACOverrides[d.address.upper()]
currentScan.append(d)
break

Expand Down Expand Up @@ -3589,13 +3628,31 @@ async def connectToLight(selectedLight, updateGUI=True):
command = ["system_profiler", "SPBluetoothDataType"]
output = run(command, stdout=PIPE, universal_newlines=True)
# get the location in the above output dealing with the specific light we're working with
light_offset = output.stdout.find(availableLights[selectedLight][0].realname)
# find the address adjacent from the above location
address_offset = output.stdout.find("Address: ", light_offset)
# clip out the MAC address itself
output_parse = output.stdout[address_offset + 9:address_offset + 26]

availableLights[selectedLight][0].HWMACaddr = output_parse
search_names = [availableLights[selectedLight][0].realname, availableLights[selectedLight][0].name]
output_parse = ""

for search_name in search_names:
if search_name != None and search_name != "":
light_offset = output.stdout.find(search_name)
if light_offset != -1:
# find the address adjacent from the above location
address_offset = output.stdout.find("Address: ", light_offset)
if address_offset != -1:
# clip out the MAC address itself
output_parse = output.stdout[address_offset + 9:address_offset + 26]
break

if output_parse != "" and ":" in output_parse:
availableLights[selectedLight][0].HWMACaddr = output_parse
else:
# if a manual override exists for this light, use it
override_key = availableLights[selectedLight][0].address.upper()
if override_key in hardwareMACOverrides:
availableLights[selectedLight][0].HWMACaddr = hardwareMACOverrides[override_key]
printDebugString(">> Using hardware MAC override from --hwmac")
else:
availableLights[selectedLight][0].HWMACaddr = None
printDebugString(">> Could not locate Hardware MAC address in system_profiler output")
else: # we're on a system that uses MAC addresses, so just duplicate the information
availableLights[selectedLight][0].HWMACaddr = availableLights[selectedLight][0].address

Expand Down Expand Up @@ -4066,7 +4123,7 @@ def processCommands(listToProcess=[]):

# ARGUMENTS EACH MODE HAS ACCESS TO
# 3-17-24 - added Infinity-style effect parameters to the list after --force_instance
acceptable_arguments = ["--light", "--mode", "--temp", "--hue", "--sat", "--bri", "--intensity",
acceptable_arguments = ["--light", "--mode", "--temp", "--hue", "--sat", "--bri", "--intensity", "--hwmac",
"gm", "--scene", "--animation", "--list", "--on", "--off", "--force_instance",
"bright_min", "bright_max", "temp_min", "temp_max", "hue_min", "hue_max",
"speed", "sparks", "specialOptions"]
Expand Down Expand Up @@ -4135,6 +4192,7 @@ def processCommands(listToProcess=[]):
parser.add_argument("--on", action="store_true", help="Turn the light on")
parser.add_argument("--off", action="store_true", help="Turn the light off")
parser.add_argument("--light", default="", help="The MAC Address (XX:XX:XX:XX:XX:XX) of the light you want to send a command to or ALL to find and control all lights (only valid when also using --cli switch)")
parser.add_argument("--hwmac", default="", help="Override hardware MAC for Infinity lights on macOS (format: AA:BB:CC:DD:EE:FF)")
parser.add_argument("--mode", default="CCT", help="[DEFAULT: CCT] The current control mode - options are HSI, CCT and either ANM or SCENE")
parser.add_argument("--temp", "--temperature", default="56", help="[DEFAULT: 56(00)K] (CCT mode) - the color temperature (3200K+) to set the light to")
parser.add_argument("--hue", default="240", help="[DEFAULT: 240] (HSI mode) - the hue (0-360 degrees) to set the light to")
Expand All @@ -4155,6 +4213,8 @@ def processCommands(listToProcess=[]):
parser.add_argument("--specialoptions", "--specialOptions", default="1", help="[DEFAULT: 1] (Infinity light SCENE mode) Special options for the current scene")

args = parser.parse_args(listToProcess)
global pendingHWMacOverride
pendingHWMacOverride = args.hwmac

if args.force_instance == False: # if this value is True, then don't do anything
global anotherInstance
Expand Down Expand Up @@ -4795,6 +4855,8 @@ def loadPrefsFile(globalPrefsFile = ""):

if os.path.exists(customLightPresetsFile):
loadCustomPresets() # if there's a custom mapping for presets, then load that into memory
if os.path.exists(hardwareMACOverridesFile):
loadHardwareMACOverrides() # load any hardware MAC overrides for Infinity lights

setUpAsyncio() # set up the asyncio loop
cmdReturn = [True] # initially set to show the GUI interface over the CLI interface
Expand Down Expand Up @@ -4910,6 +4972,16 @@ def loadPrefsFile(globalPrefsFile = ""):

printDebugString("-------------------------------------------------------------------------------------")

if pendingHWMacOverride != "":
override_list = pendingHWMacOverride.upper().split(",")
if len(override_list) == 1:
hardwareMACOverrides[MACAddresses[0]] = override_list[0]
elif len(override_list) == len(MACAddresses):
for i in range(len(MACAddresses)):
hardwareMACOverrides[MACAddresses[i]] = override_list[i]
else:
printDebugString(" >> Warning: --hwmac count does not match --light count, ignoring overrides")

asyncioEventLoop.run_until_complete(findDevices(limitToDevices = MACAddresses)) # get Bleak object linking to this specific light and getting custom prefs
else:
printDebugString("-------------------------------------------------------------------------------------")
Expand Down Expand Up @@ -5021,4 +5093,4 @@ def loadPrefsFile(globalPrefsFile = ""):
printDebugString("-------------------------------------------------------------------------------------")
printDebugString(f" > CLI >> Calculated bytestring: {updateStatus()}")

singleInstanceUnlockandQuit(0) # delete the lock file and quit out
singleInstanceUnlockandQuit(0) # delete the lock file and quit out
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ NeewerLite-Python is an un-official cross-platform Neewer LED light control app

Read the manual here: https://github.com/taburineagle/NeewerLite-Python/wiki

## macOS Infinity lights: hardware MAC override
Some Infinity-protocol lights on macOS report a BLE GUID instead of a hardware MAC. If you see errors about a missing hardware MAC, you can add a persistent override file:

1. Copy `hardwareMACs.prefs.example` to `light_prefs/hardwareMACs.prefs`
2. Add one mapping per line in the format `GUID=AA:BB:CC:DD:EE:FF`
3. Restart NeewerLite-Python (GUI/HTTP/CLI) to load the overrides

Note: I observed this on two TL60 lights where one advertised as `TL60 RGB` and required a hardware MAC override on macOS, while the other advertised as `NW-20240061&92A10500` and worked without an override.

**Added default settings for these lights (not all of these lights are Bluetooth controllable, so... your mileage may very):** GL1, NL140 SNL1320, SNL1920, SNL480, SNL530, **SNL660**, SNL960, SRP16, SRP18, WRP18, ZRP16, BH30S, CB60, CL124, RGB C80, RGB CB60, RGB1000, RGB1200, RGB140, RGB168, RGB176 A1, RGB512, RGB800, SL-90, RGB1, **RGB176**, RGB18, RGB190, RGB450, **RGB480**, RGB530 PRO, RGB530, RGB650, **RGB660 PRO**, RGB660, RGB960, RGB-P200, RGB-P280, SL-70, **SL-80**, ZK-RY

**Fully tested Neewer lights (in bold above) so far:** SL-80, SNL-660, RGB660 PRO, 480 RGB, RGB176
Expand Down
3 changes: 3 additions & 0 deletions hardwareMACs.prefs.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GUID=HARDWARE_MAC (used for Infinity lights on macOS)
# Example:
# 2FA67438-C43E-D3BD-0F88-9DB578BC0518=E4:08:07:C4:F2:34