<commit_before>#******************************************************************************
# (C) 2018, Stefan Korner, Austria                                            *
#                                                                             *
# The Space Python Library is free software; you can redistribute it and/or   *
# modify it under under the terms of the MIT License as published by the      *
# Massachusetts Institute of Technology.                                      *
#                                                                             *
# The Space Python Library is distributed in the hope that it will be useful, *
# but WITHOUT ANY WARRANTY; without even the implied warranty of              *
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MIT License    *
# for more details.                                                           *
#******************************************************************************
# Monitoring and Control (M&C) - Control (TC) GUI                             *
#******************************************************************************
import tkinter
from tkinter import simpledialog
from UTIL.SYS import Error, LOG, LOG_INFO, LOG_WARNING, LOG_ERROR
import MC.IF
import PUS.VP
import SUPP.IF
import SUPPUI.VPgui
import UI.TKI

#############
# constants #
#############
COLOR_BUTTON_FG = "#FFFFFF"
COLOR_BUTTON_BG = "#808080"
COLOR_INITIALISED = "#FFFF00"
COLOR_CONNECTED = "#00FF00"

###########
# classes #
###########
# =============================================================================
class TCpacketDetails(tkinter.Frame, UI.TKI.AppGrid):
  """Displays the packet details, implemented as tkinter.Frame"""
  # ---------------------------------------------------------------------------
  def __init__(self, master):
    tkinter.Frame.__init__(self, master, relief=tkinter.GROOVE, borderwidth=1)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=0, columnspan=2, rowweight=0)
    # packet name
    self.pktNameField = UI.TKI.ValueField(self, row=1, label="Packet name:")
    # packet description
    self.pktDescrField = UI.TKI.ValueField(self, row=2, label="Packet description:")
    # packet description 2
    self.pktDescrField2 = UI.TKI.ValueField(self, row=3, label="Packet description 2:")
    # APID
    self.pktAPIDfield = UI.TKI.ValueField(self, row=4, label="Packet APID:")
    # Type
    self.pktTypeField = UI.TKI.ValueField(self, row=5, label="Packet Type:")
    # Subtype
    self.pktSubtypeField = UI.TKI.ValueField(self, row=6, label="Packet Subtype:")
    # PI1
    self.pktPI1field = UI.TKI.ValueField(self, row=7, label="Packet PI1:")
    # PI2
    self.pktPI2field = UI.TKI.ValueField(self, row=8, label="Packet PI2:")
    # --- parameter tree ---
    label = tkinter.Label(self, text="Parameters")
    self.appGrid(label, row=0, column=2, rowweight=0)
    self.parametersTreeview = SUPPUI.VPgui.TreeView(self)
    self.appGrid(self.parametersTreeview, row=1, column=2, rowspan=8, rowweight=0, columnweight=1)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=9, columnspan=3, rowweight=0)
    # --- route ---
    self.routeField = UI.TKI.InputField(self, row=10, label="route:")
    self.appGrid(self.routeField.field, row=10, column=1, columnspan=2, rowweight=0)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=11, columnspan=3, rowweight=0)
    # TC Struct for variable packet parameters
    self.tcStruct = None
  # ---------------------------------------------------------------------------
  def update(self, tcPktDef):
    """Update the packet fields"""
    # fetch the data
    pktName = ""
    pktDescr = ""
    pktDescr2 = ""
    pktAPID = ""
    pktType = ""
    pktSType = ""
    pktPI1val = ""
    pktPI2val = ""
    tcStructDef = None
    if tcPktDef != None:
      pktName = tcPktDef.pktName
      pktDescr = tcPktDef.pktDescr
      pktDescr2 = tcPktDef.pktDescr2
      pktAPID = tcPktDef.pktAPID
      pktType = tcPktDef.pktType
      pktSType = tcPktDef.pktSType
      if tcPktDef.pktPI1val != None:
        pktPI1val = tcPktDef.pktPI1val
      if tcPktDef.pktPI2val != None:
        pktPI2val = tcPktDef.pktPI2val
      tcStructDef = tcPktDef.tcStructDef
    # write the data into the GUI
    self.pktNameField.set(pktName)
    self.pktDescrField.set(pktDescr)
    self.pktDescrField2.set(pktDescr2)
    self.pktAPIDfield.set(pktAPID)
    self.pktTypeField.set(pktType)
    self.pktSubtypeField.set(pktSType)
    self.pktPI1field.set(pktPI1val)
    self.pktPI2field.set(pktPI2val)
    self.tcStruct = PUS.VP.Struct(tcStructDef)
    self.parametersTreeview.fillTree(pktName, self.tcStruct)

# =============================================================================
class TCpacketBrowser(simpledialog.Dialog, UI.TKI.AppGrid):
  """Browser for TC packets"""
  # ---------------------------------------------------------------------------
  def __init__(self, master, title, prompt=""):
    """Read the MIB for obtaining the initialisation data"""
    # initialise the dialog
    self.prompt = prompt
    simpledialog.Dialog.__init__(self, master, title=title)
  # ---------------------------------------------------------------------------
  def body(self, master):
    """Intialise the dialog"""
    row=0
    if self.prompt != "":
      label = tkinter.Label(master, text=self.prompt)
      label.grid(row=row, column=0, columnspan=4)
      row += 1
      label = tkinter.Label(master)
      label.grid(row=row, column=0, columnspan=4)
      row += 1
    # scrolled list box
    self.slistbox = UI.TKI.ScrolledListbox(master, self.packetSelected)
    self.appGrid(self.slistbox, row=row, column=0, columnweight=1)
    lrow = 0
    for tcPktDef in SUPP.IF.s_definitions.getTCpktDefs():
      packetName = tcPktDef.pktName
      self.insertListboxRow(lrow, packetName)
      lrow += 1
    # details
    self.details = TCpacketDetails(master)
    self.appGrid(self.details, row=row, column=1, columnweight=0)
  # ---------------------------------------------------------------------------
  def insertListboxRow(self, row, text):
    """Inserts a row into self.slistbox"""
    self.slistbox.list().insert(row, text)
  # ---------------------------------------------------------------------------
  def packetSelected(self, selectPos):
    """Callback when packet is selected"""
    # display the packet data
    tcPktDef = SUPP.IF.s_definitions.getTCpktDefByIndex(selectPos)
    self.details.update(tcPktDef)
  # ---------------------------------------------------------------------------
  def apply(self):
    """Callback when the OK button is pressed"""
    packetName = self.details.pktNameField.get()
    if packetName != "":
      route = self.details.routeField.get()
      tcStruct = self.details.tcStruct
      self.result = [packetName, route, tcStruct]

# =============================================================================
class GUIview(UI.TKI.GUItabView):
  """Implementation of the M&C Control layer"""
  # ---------------------------------------------------------------------------
  def __init__(self, master):
    """Initialise all GUI elements"""
    UI.TKI.GUItabView.__init__(self, master, "TC", "M&C TC")
    # menu buttons
    self.menuButtons = UI.TKI.MenuButtons(self,
      [["PKT", self.setPacketDataCallback, COLOR_BUTTON_FG, COLOR_BUTTON_BG, tkinter.DISABLED],
       ["SND", self.sendPacketCallback, COLOR_BUTTON_FG, COLOR_BUTTON_BG, tkinter.DISABLED]])
    self.appGrid(self.menuButtons,
                 row=0,
                 columnspan=2,
                 rowweight=0,
                 sticky=tkinter.EW)
    # tc status
    self.tcStatusField = UI.TKI.ValueField(self, row=1, label="TC status:")
    self.tcStatusField.set("INIT")
    self.tcStatusField.setBackground(COLOR_INITIALISED)
    # packet
    self.packetField = UI.TKI.ValueField(self, row=2, label="Packet:")
    # route
    self.routeField = UI.TKI.ValueField(self, row=3, label="Route:")
    # log messages (default logger)
    self.messageLogger = UI.TKI.MessageLogger(self, "TC")
    self.appGrid(self.messageLogger, row=4, columnspan=2)
    # message line
    self.messageline = tkinter.Message(self, relief=tkinter.GROOVE)
    self.appGrid(self.messageline,
                 row=5,
                 columnspan=2,
                 rowweight=0,
                 columnweight=0,
                 sticky=tkinter.EW)
    self.grid(row=0, column=0, sticky=tkinter.EW+tkinter.NS)
    self.master.rowconfigure(0, weight=1)
    self.master.columnconfigure(0, weight=1)
  # ---------------------------------------------------------------------------
  def fillCommandMenuItems(self):
    """
    fill the command menu bar,
    implementation of UI.TKI.GUItabView.fillCommandMenuItems
    """
    self.addCommandMenuItem(label="SetPacketData", command=self.setPacketDataCallback, enabled=False)
    self.addCommandMenuItem(label="SendPacket", command=self.sendPacketCallback, enabled=False)
  # ---------------------------------------------------------------------------
  def setPacketDataCallback(self):
    """Called when the SetPacketData menu entry is selected"""
    # do the dialog
    dialog = TCpacketBrowser(self,
      title="Set Packet Data Dialog",
      prompt="Please select a packet.")
    #if dialog.result != None:
    #  packetName, route, tcStruct = dialog.result
    #  self.notifyModelTask(["SETPACKETDATA", packetName, route], tcStruct)
  # ---------------------------------------------------------------------------
  def sendPacketCallback(self):
    """Called when the SendPacket menu entry is selected"""
    self.notifyModelTask(["SENDPACKET"])
  # ---------------------------------------------------------------------------
  def notifyStatus(self, status):
    """Generic callback when something changes in the model"""
    if status == "TC_CONNECTED":
      self.tcConnectedNotify()
    elif status == "PACKETDATA_SET":
      self.packetDataSetNotify()
  # ---------------------------------------------------------------------------
  def tcConnectedNotify(self):
    """Called when the TC connect function is successfully processed"""
    self.enableCommandMenuItem("SetPacketData")
    self.menuButtons.setState("PKT", tkinter.NORMAL)
    self.updateTCstatusField()
  # ---------------------------------------------------------------------------
  def packetDataSetNotify(self):
    """Called when the setPacketData function is successfully processed"""
    self.enableCommandMenuItem("SendPacket")
    self.menuButtons.setState("SND", tkinter.NORMAL)
    self.updateTCstatusField()
    self.packetField.set(MC.IF.s_configuration.tcPacketData.pktName)
    self.routeField.set(MC.IF.s_configuration.tcPacketData.route)
  # ---------------------------------------------------------------------------
  def updateTCstatusField(self):
    """updated the TC status field depending on the MC.IF.s_configuration"""
    if MC.IF.s_configuration.connected:
      txt = "CONNECTED"
      bgColor = COLOR_CONNECTED
    else:
      txt = "INIT"
    if MC.IF.s_configuration.tcPacketData != None:
      txt += " + PKT DEFINED"
    self.tcStatusField.set(txt)
    if MC.IF.s_configuration.connected:
      self.tcStatusField.setBackground(bgColor)
<commit_msg>comment-in the TC packet editing result proessing<commit_after>#******************************************************************************
# (C) 2018, Stefan Korner, Austria                                            *
#                                                                             *
# The Space Python Library is free software; you can redistribute it and/or   *
# modify it under under the terms of the MIT License as published by the      *
# Massachusetts Institute of Technology.                                      *
#                                                                             *
# The Space Python Library is distributed in the hope that it will be useful, *
# but WITHOUT ANY WARRANTY; without even the implied warranty of              *
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MIT License    *
# for more details.                                                           *
#******************************************************************************
# Monitoring and Control (M&C) - Control (TC) GUI                             *
#******************************************************************************
import tkinter
from tkinter import simpledialog
from UTIL.SYS import Error, LOG, LOG_INFO, LOG_WARNING, LOG_ERROR
import MC.IF
import PUS.VP
import SUPP.IF
import SUPPUI.VPgui
import UI.TKI

#############
# constants #
#############
COLOR_BUTTON_FG = "#FFFFFF"
COLOR_BUTTON_BG = "#808080"
COLOR_INITIALISED = "#FFFF00"
COLOR_CONNECTED = "#00FF00"

###########
# classes #
###########
# =============================================================================
class TCpacketDetails(tkinter.Frame, UI.TKI.AppGrid):
  """Displays the packet details, implemented as tkinter.Frame"""
  # ---------------------------------------------------------------------------
  def __init__(self, master):
    tkinter.Frame.__init__(self, master, relief=tkinter.GROOVE, borderwidth=1)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=0, columnspan=2, rowweight=0)
    # packet name
    self.pktNameField = UI.TKI.ValueField(self, row=1, label="Packet name:")
    # packet description
    self.pktDescrField = UI.TKI.ValueField(self, row=2, label="Packet description:")
    # packet description 2
    self.pktDescrField2 = UI.TKI.ValueField(self, row=3, label="Packet description 2:")
    # APID
    self.pktAPIDfield = UI.TKI.ValueField(self, row=4, label="Packet APID:")
    # Type
    self.pktTypeField = UI.TKI.ValueField(self, row=5, label="Packet Type:")
    # Subtype
    self.pktSubtypeField = UI.TKI.ValueField(self, row=6, label="Packet Subtype:")
    # PI1
    self.pktPI1field = UI.TKI.ValueField(self, row=7, label="Packet PI1:")
    # PI2
    self.pktPI2field = UI.TKI.ValueField(self, row=8, label="Packet PI2:")
    # --- parameter tree ---
    label = tkinter.Label(self, text="Parameters")
    self.appGrid(label, row=0, column=2, rowweight=0)
    self.parametersTreeview = SUPPUI.VPgui.TreeView(self)
    self.appGrid(self.parametersTreeview, row=1, column=2, rowspan=8, rowweight=0, columnweight=1)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=9, columnspan=3, rowweight=0)
    # --- route ---
    self.routeField = UI.TKI.InputField(self, row=10, label="route:")
    self.appGrid(self.routeField.field, row=10, column=1, columnspan=2, rowweight=0)
    # --- filler ---
    filler = tkinter.Label(self)
    self.appGrid(filler, row=11, columnspan=3, rowweight=0)
    # TC Struct for variable packet parameters
    self.tcStruct = None
  # ---------------------------------------------------------------------------
  def update(self, tcPktDef):
    """Update the packet fields"""
    # fetch the data
    pktName = ""
    pktDescr = ""
    pktDescr2 = ""
    pktAPID = ""
    pktType = ""
    pktSType = ""
    pktPI1val = ""
    pktPI2val = ""
    tcStructDef = None
    if tcPktDef != None:
      pktName = tcPktDef.pktName
      pktDescr = tcPktDef.pktDescr
      pktDescr2 = tcPktDef.pktDescr2
      pktAPID = tcPktDef.pktAPID
      pktType = tcPktDef.pktType
      pktSType = tcPktDef.pktSType
      if tcPktDef.pktPI1val != None:
        pktPI1val = tcPktDef.pktPI1val
      if tcPktDef.pktPI2val != None:
        pktPI2val = tcPktDef.pktPI2val
      tcStructDef = tcPktDef.tcStructDef
    # write the data into the GUI
    self.pktNameField.set(pktName)
    self.pktDescrField.set(pktDescr)
    self.pktDescrField2.set(pktDescr2)
    self.pktAPIDfield.set(pktAPID)
    self.pktTypeField.set(pktType)
    self.pktSubtypeField.set(pktSType)
    self.pktPI1field.set(pktPI1val)
    self.pktPI2field.set(pktPI2val)
    self.tcStruct = PUS.VP.Struct(tcStructDef)
    self.parametersTreeview.fillTree(pktName, self.tcStruct)

# =============================================================================
class TCpacketBrowser(simpledialog.Dialog, UI.TKI.AppGrid):
  """Browser for TC packets"""
  # ---------------------------------------------------------------------------
  def __init__(self, master, title, prompt=""):
    """Read the MIB for obtaining the initialisation data"""
    # initialise the dialog
    self.prompt = prompt
    simpledialog.Dialog.__init__(self, master, title=title)
  # ---------------------------------------------------------------------------
  def body(self, master):
    """Intialise the dialog"""
    row=0
    if self.prompt != "":
      label = tkinter.Label(master, text=self.prompt)
      label.grid(row=row, column=0, columnspan=4)
      row += 1
      label = tkinter.Label(master)
      label.grid(row=row, column=0, columnspan=4)
      row += 1
    # scrolled list box
    self.slistbox = UI.TKI.ScrolledListbox(master, self.packetSelected)
    self.appGrid(self.slistbox, row=row, column=0, columnweight=1)
    lrow = 0
    for tcPktDef in SUPP.IF.s_definitions.getTCpktDefs():
      packetName = tcPktDef.pktName
      self.insertListboxRow(lrow, packetName)
      lrow += 1
    # details
    self.details = TCpacketDetails(master)
    self.appGrid(self.details, row=row, column=1, columnweight=0)
  # ---------------------------------------------------------------------------
  def insertListboxRow(self, row, text):
    """Inserts a row into self.slistbox"""
    self.slistbox.list().insert(row, text)
  # ---------------------------------------------------------------------------
  def packetSelected(self, selectPos):
    """Callback when packet is selected"""
    # display the packet data
    tcPktDef = SUPP.IF.s_definitions.getTCpktDefByIndex(selectPos)
    self.details.update(tcPktDef)
  # ---------------------------------------------------------------------------
  def apply(self):
    """Callback when the OK button is pressed"""
    packetName = self.details.pktNameField.get()
    if packetName != "":
      route = self.details.routeField.get()
      tcStruct = self.details.tcStruct
      self.result = [packetName, route, tcStruct]

# =============================================================================
class GUIview(UI.TKI.GUItabView):
  """Implementation of the M&C Control layer"""
  # ---------------------------------------------------------------------------
  def __init__(self, master):
    """Initialise all GUI elements"""
    UI.TKI.GUItabView.__init__(self, master, "TC", "M&C TC")
    # menu buttons
    self.menuButtons = UI.TKI.MenuButtons(self,
      [["PKT", self.setPacketDataCallback, COLOR_BUTTON_FG, COLOR_BUTTON_BG, tkinter.DISABLED],
       ["SND", self.sendPacketCallback, COLOR_BUTTON_FG, COLOR_BUTTON_BG, tkinter.DISABLED]])
    self.appGrid(self.menuButtons,
                 row=0,
                 columnspan=2,
                 rowweight=0,
                 sticky=tkinter.EW)
    # tc status
    self.tcStatusField = UI.TKI.ValueField(self, row=1, label="TC status:")
    self.tcStatusField.set("INIT")
    self.tcStatusField.setBackground(COLOR_INITIALISED)
    # packet
    self.packetField = UI.TKI.ValueField(self, row=2, label="Packet:")
    # route
    self.routeField = UI.TKI.ValueField(self, row=3, label="Route:")
    # log messages (default logger)
    self.messageLogger = UI.TKI.MessageLogger(self, "TC")
    self.appGrid(self.messageLogger, row=4, columnspan=2)
    # message line
    self.messageline = tkinter.Message(self, relief=tkinter.GROOVE)
    self.appGrid(self.messageline,
                 row=5,
                 columnspan=2,
                 rowweight=0,
                 columnweight=0,
                 sticky=tkinter.EW)
    self.grid(row=0, column=0, sticky=tkinter.EW+tkinter.NS)
    self.master.rowconfigure(0, weight=1)
    self.master.columnconfigure(0, weight=1)
  # ---------------------------------------------------------------------------
  def fillCommandMenuItems(self):
    """
    fill the command menu bar,
    implementation of UI.TKI.GUItabView.fillCommandMenuItems
    """
    self.addCommandMenuItem(label="SetPacketData", command=self.setPacketDataCallback, enabled=False)
    self.addCommandMenuItem(label="SendPacket", command=self.sendPacketCallback, enabled=False)
  # ---------------------------------------------------------------------------
  def setPacketDataCallback(self):
    """Called when the SetPacketData menu entry is selected"""
    # do the dialog
    dialog = TCpacketBrowser(self,
      title="Set Packet Data Dialog",
      prompt="Please select a packet.")
    if dialog.result != None:
      packetName, route, tcStruct = dialog.result
      self.notifyModelTask(["SETPACKETDATA", packetName, route], tcStruct)
  # ---------------------------------------------------------------------------
  def sendPacketCallback(self):
    """Called when the SendPacket menu entry is selected"""
    self.notifyModelTask(["SENDPACKET"])
  # ---------------------------------------------------------------------------
  def notifyStatus(self, status):
    """Generic callback when something changes in the model"""
    if status == "TC_CONNECTED":
      self.tcConnectedNotify()
    elif status == "PACKETDATA_SET":
      self.packetDataSetNotify()
  # ---------------------------------------------------------------------------
  def tcConnectedNotify(self):
    """Called when the TC connect function is successfully processed"""
    self.enableCommandMenuItem("SetPacketData")
    self.menuButtons.setState("PKT", tkinter.NORMAL)
    self.updateTCstatusField()
  # ---------------------------------------------------------------------------
  def packetDataSetNotify(self):
    """Called when the setPacketData function is successfully processed"""
    self.enableCommandMenuItem("SendPacket")
    self.menuButtons.setState("SND", tkinter.NORMAL)
    self.updateTCstatusField()
    self.packetField.set(MC.IF.s_configuration.tcPacketData.pktName)
    self.routeField.set(MC.IF.s_configuration.tcPacketData.route)
  # ---------------------------------------------------------------------------
  def updateTCstatusField(self):
    """updated the TC status field depending on the MC.IF.s_configuration"""
    if MC.IF.s_configuration.connected:
      txt = "CONNECTED"
      bgColor = COLOR_CONNECTED
    else:
      txt = "INIT"
    if MC.IF.s_configuration.tcPacketData != None:
      txt += " + PKT DEFINED"
    self.tcStatusField.set(txt)
    if MC.IF.s_configuration.connected:
      self.tcStatusField.setBackground(bgColor)
<|endoftext|>


<commit_before>import os
import sys

import numpy as np

from neural import TrainingLoader
from neural.SimpleLayeredNeuralNetwork import SimpleLayeredNeuralNetwork
from neural.SimpleNeuralNetwork import SimpleNeuralNetwork

def main(argv):
    opt_tries = 1000
    opt_cycles = 30001
    opt_multi = True
    opt_multi_neural_num = 10
    opt_every = True
    opt_step = 1000
    opt_max_error = 0.2734375
    opt_folder = 'tmp/search/'
    opt_input_file = 'resource/life_all'
    opt_weights_file = None

    # TODO Add specific settings to enable learning from weights file with mutations

    if len(argv) > 0:
        opt_tries = int(argv[0])
        opt_cycles = int(argv[1])
        opt_multi = str(argv[2]).lower() == 't'
        opt_multi_neural_num = int(argv[3])
        opt_every = str(argv[4]).lower() == 't'
        opt_step = int(argv[5])
        opt_max_error = float(argv[6])
        if len(argv) > 7:
            opt_weights_file = argv[7]

    training_set_tmp = TrainingLoader.load(opt_input_file)

    inputs = np.array(training_set_tmp[0])
    outputs = np.array([training_set_tmp[1]]).T

    os.makedirs(opt_folder, exist_ok=True)

    best_of_bests = 1

    if opt_multi:
        network = SimpleLayeredNeuralNetwork()
        network.add_layer(opt_multi_neural_num, len(training_set_tmp[0][0]))
        network.add_layer(1, opt_multi_neural_num)
    else:
        network = SimpleNeuralNetwork(len(training_set_tmp[0][0]))

    opt_folder = opt_folder + '/' + '.'.join(
        [str(layer.number_of_neurons) + '-' + str(layer.number_of_inputs_per_neuron) for layer in network.layers]
    )

    last_network = None

    for i in range(opt_tries):
        print("Iteration: " + str(i + 1))

        np.random.seed()
        if opt_weights_file is None:
            network.rebuild()
        else:
            network.read_synaptic_weights(opt_weights_file)

        last_error = 0

        best_weights = None

        for j in range(opt_cycles):
            network.train_once(inputs, outputs)
            if (j % opt_step) == 0:
                if last_error == network.error:
                    # TODO Make mutation instead of breaking
                    print('\tBreak - no progress')
                    break
                if network.error < opt_max_error:
                    if network.error < last_error:
                        best_weights = network.get_weights()
                last_error = network.error

        last_network = network

        if opt_every and best_weights:
            print("\tError: " + str(network.error))
            network.save_synaptic_weights(opt_folder + '/' + str(network.error) + '.txt')
        if network.error < best_of_bests and network.error < opt_max_error:
            print("\tCurrent best error: " + str(network.error))
            best_of_bests = network.error
            network.save_synaptic_weights(opt_folder + '/' + str(network.error) + '.txt')

if __name__ == "__main__":
    main(sys.argv[1:])
<commit_msg>MainSearch - Support for multi-layered networks and mutations when no progress<commit_after>import os
import sys

import numpy as np

from neural import TrainingLoader
from neural.SimpleLayeredNeuralNetwork import SimpleLayeredNeuralNetwork

def main(argv):
    opt_tries = 1000
    opt_cycles = 30001
    opt_multi_neural = '9-2'
    opt_every = True
    opt_step = 1000
    opt_max_error = 0.2734375
    opt_mutations = 5
    opt_mutation_value = .05
    opt_folder = 'tmp/search/'
    opt_input_file = 'resource/life_all'
    opt_weights_file = None

    # TODO Add specific settings to enable learning from weights file with mutations

    if len(argv) > 0:
        opt_tries = int(argv[0])
        opt_cycles = int(argv[1])
        opt_multi_neural = int(argv[2])
        opt_every = str(argv[3]).lower() == 't'
        opt_step = int(argv[4])
        opt_max_error = float(argv[5])
        opt_mutations = int(argv[6]) if argv[5].isdigit() else 0
        opt_mutation_value = float(argv[7])
        if len(argv) > 8:
            opt_weights_file = argv[8]

    training_set_tmp = TrainingLoader.load(opt_input_file)

    inputs = np.array(training_set_tmp[0])
    outputs = np.array([training_set_tmp[1]]).T

    os.makedirs(opt_folder, exist_ok=True)

    best_of_bests = 1

    network = SimpleLayeredNeuralNetwork()

    opt_multi_neural = opt_multi_neural.split('-')

    if len(opt_multi_neural) == 1 and int(opt_multi_neural[0]) == 1:
        network.add_layer(1, len(training_set_tmp[0][0]))
    else:
        network.add_layer(int(opt_multi_neural[0]), len(training_set_tmp[0][0]))
        for idx, val in enumerate(opt_multi_neural[1:]):
            network.add_layer(int(val), int(opt_multi_neural[idx - 1]))
        network.add_layer(1, int(opt_multi_neural[-1]))

    opt_folder = opt_folder + '/' + '.'.join(
        [str(layer.number_of_neurons) + '-' + str(layer.number_of_inputs_per_neuron) for layer in network.layers]
    )

    for i in range(opt_tries):
        print("Iteration: " + str(i + 1))

        np.random.seed()
        if opt_weights_file is None:
            network.rebuild()
        else:
            network.read_synaptic_weights(opt_weights_file)

        last_error = 0

        best_weights = None

        for j in range(opt_cycles):
            network.train_once(inputs, outputs)
            if (j % opt_step) == 0:
                if last_error == network.error:
                    if opt_mutations == 0:
                        print('\tBreak - no progress')
                        break
                    else:
                        print('\tMutating %d weights' % opt_mutations)
                        for x in range(opt_mutations):
                            layer = np.random.choice(network.layers)
                            num = np.random.randint(0, len(layer.weights))
                            if np.random.choice([True, False]):
                                layer.weights[num] *= (1.0 + opt_mutation_value)
                            else:
                                layer.weights[num] *= (1.0 - opt_mutation_value)
                        break
                if network.error < opt_max_error:
                    if network.error < last_error:
                        best_weights = network.get_weights()
                last_error = network.error

        if opt_every and best_weights:
            print("\tError: " + str(network.error))
            network.save_synaptic_weights(opt_folder + '/' + str(network.error) + '.txt')
        if network.error < best_of_bests and network.error < opt_max_error:
            print("\tCurrent best error: " + str(network.error))
            best_of_bests = network.error
            network.save_synaptic_weights(opt_folder + '/' + str(network.error) + '.txt')

if __name__ == "__main__":
    main(sys.argv[1:])
<|endoftext|>


<commit_before>#!/usr/bin/env python

from scipy import arange, array, corrcoef, mean, random, spatial, stats, std, zeros

# MantelTest()
#   Takes two lists of pairwise distances and performs a Mantel test. Returns
#   the veridical correlation (r), the mean (m) and standard deviation (sd)
#   of the Monte Carlo sample correlations, a Z-score (z) quantifying the
#   significance of the veridical correlation, and a p-value for a normality
#   test on the distribution of sample correlations (norm).

def MantelTest(distances1, distances2, randomizations=10000):
  if ValidateInput(distances1, distances2, randomizations) == False:
    return None
  vector1 = array(distances1, dtype=float)
  vector2 = array(distances2, dtype=float)
  r = corrcoef([vector1, vector2])[0, 1]
  m, sd, norm = MonteCarlo(vector1, vector2, randomizations)
  z = (r-m)/sd
  return r, m, sd, z, norm

# MonteCarlo()
#   Takes two vectors. Measures the correlation between vector 1 and vector 2
#   many times, shuffling vector 2 on each iteration. Returns the mean and
#   standard deviation of the correlations, and a p-value for a normality test
#   of the distribution of correlations.

def MonteCarlo(vector1, vector2, randomizations):
  correlations = zeros(randomizations, dtype=float)
  for i in xrange(0, randomizations):
    correlations[i] = corrcoef([vector1, MatrixShuffle(vector2)])[0, 1]
  return mean(correlations), std(correlations), stats.normaltest(correlations)[1]

# MatrixShuffle()
#   Takes a vector, converts it to a distance matrix, shuffles the matrix, and
#   returns the upper triangle as a vector.

def MatrixShuffle(vector):
  matrix = spatial.distance.squareform(vector, 'tomatrix')
  shuffled_vector = zeros(vector.shape[0], dtype=float)
  n = matrix.shape[0]
  shuffle_order = arange(n, dtype=int)
  random.shuffle(shuffle_order)
  c = 0
  for i in xrange(0, n-1):
    for j in xrange(i+1, n):
      shuffled_vector[c] = matrix[shuffle_order[i], shuffle_order[j]]
      c += 1
  return shuffled_vector

# ValidateInput()
#   Validates input arguments and returns an error message if a problem is
#   identified. Returns True otherwise.

def ValidateInput(distances1, distances2, randomizations):
  if type(randomizations) == int:
    if type(distances1) == list and type(distances2) == list:
      if len(distances1) == len(distances2):
        if spatial.distance.is_valid_y(array(distances1, dtype=float)) == True:
          if spatial.distance.is_valid_y(array(distances2, dtype=float)) == True:
            return True
        print('Error: the sets of pairwise distances are invalid')
        return False
      print('Error: the sets of distances should be of the same length')
      return False
    print('Error: the sets of distances should be lists')
    return False
  print('Error: the number of randomizations should be an integer')
  return False
<commit_msg>Various changes taken in from MantelTest repo<commit_after>#!/usr/bin/env python

from scipy import arange, array, mean, random, spatial, stats, std, zeros

# MantelTest()
#   Takes two lists of pairwise distances and performs a Mantel test. Returns
#   the veridical correlation (r), the mean (m) and standard deviation (sd)
#   of the Monte Carlo sample correlations, a Z-score (z) quantifying the
#   significance of the veridical correlation, and a p-value for a normality
#   test on the distribution of sample correlations (norm).

def MantelTest(distances1, distances2, randomizations=10000, correlation_measure='pearson'):
  ValidateInput(distances1, distances2, randomizations, correlation_measure)
  vector1 = array(distances1, dtype=float)
  vector2 = array(distances2, dtype=float)
  r, p = Correlate(vector1, vector2, correlation_measure)
  m, sd, norm = MonteCarlo(vector1, vector2, randomizations, correlation_measure)
  z = (r-m)/sd
  return r, p, m, sd, z, norm

# Correlate()
#   Correlates two vectors using a given type of correlation coefficient

def Correlate(vector1, vector2, correlation_measure):
  if correlation_measure == 'pearson':
    return stats.pearsonr(vector1, vector2)
  elif correlation_measure == 'spearman':
    return stats.spearmanr(vector1, vector2)
  elif correlation_measure == 'kendall':
    return stats.kendalltau(vector1, vector2)
  else:
    raise ValueError('The correlation_measure should be set to "pearson", "spearman", or "kendall"')

# MonteCarlo()
#   Takes two vectors. Measures the correlation between vector 1 and vector 2
#   many times, shuffling vector 2 on each iteration. Returns the mean and
#   standard deviation of the correlations, and a p-value for a normality test
#   of the distribution of correlations.

def MonteCarlo(vector1, vector2, randomizations, correlation_measure):
  correlations = zeros(randomizations, dtype=float)
  for i in xrange(0, randomizations):
    correlations[i] = Correlate(vector1, MatrixShuffle(vector2), correlation_measure)[0]
  return mean(correlations), std(correlations), stats.normaltest(correlations)[1]

# MatrixShuffle()
#   Takes a vector, converts it to a distance matrix, shuffles the matrix, and
#   returns the upper triangle as a vector.

def MatrixShuffle(vector):
  matrix = spatial.distance.squareform(vector, 'tomatrix')
  shuffled_vector = zeros(vector.shape[0], dtype=float)
  n = matrix.shape[0]
  shuffle_order = arange(n, dtype=int)
  random.shuffle(shuffle_order)
  c = 0
  for i in xrange(0, n-1):
    for j in xrange(i+1, n):
      shuffled_vector[c] = matrix[shuffle_order[i], shuffle_order[j]]
      c += 1
  return shuffled_vector

# ValidateInput()
#   Validates input arguments and raises an error if a problem is identified.

def ValidateInput(distances1, distances2, randomizations, correlation_measure):
  if type(randomizations) != int:
    raise ValueError('The number of randomizations should be an integer')
  if type(correlation_measure) != str or correlation_measure not in ['pearson', 'spearman', 'kendall']:
    raise ValueError('The correlation_measure should be set to "pearson", "spearman", or "kendall"')
  if type(distances1) != list or type(distances2) != list:
    raise ValueError('The sets of pairise distances should be Python lists')
  if len(distances1) != len(distances2):
    raise ValueError('The two sets of pairwise distances should be of the same length')
  if spatial.distance.is_valid_y(array(distances1, dtype=float)) == False:
    raise ValueError('The first set of pairwise distances is invalid')
  if spatial.distance.is_valid_y(array(distances2, dtype=float)) == False:
    raise ValueError('The second set of pairwise distances is invalid')
<|endoftext|>


<commit_before>"""This component provides support for RainMachine programs and zones."""
from __future__ import annotations

import asyncio
from collections.abc import Coroutine
from dataclasses import dataclass
from datetime import datetime
from typing import Any

from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError
import voluptuous as vol

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from . import RainMachineEntity, async_update_programs_and_zones
from .const import (
    CONF_ZONE_RUN_TIME,
    DATA_CONTROLLER,
    DATA_COORDINATOR,
    DATA_PROGRAMS,
    DATA_ZONES,
    DEFAULT_ZONE_RUN,
    DOMAIN,
    RUN_STATE_MAP,
)
from .model import RainMachineDescriptionMixinUid

ATTR_AREA = "area"
ATTR_CS_ON = "cs_on"
ATTR_CURRENT_CYCLE = "current_cycle"
ATTR_CYCLES = "cycles"
ATTR_DELAY = "delay"
ATTR_DELAY_ON = "delay_on"
ATTR_FIELD_CAPACITY = "field_capacity"
ATTR_NEXT_RUN = "next_run"
ATTR_NO_CYCLES = "number_of_cycles"
ATTR_PRECIP_RATE = "sprinkler_head_precipitation_rate"
ATTR_RESTRICTIONS = "restrictions"
ATTR_SLOPE = "slope"
ATTR_SOAK = "soak"
ATTR_SOIL_TYPE = "soil_type"
ATTR_SPRINKLER_TYPE = "sprinkler_head_type"
ATTR_STATUS = "status"
ATTR_SUN_EXPOSURE = "sun_exposure"
ATTR_VEGETATION_TYPE = "vegetation_type"
ATTR_ZONES = "zones"

DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

SOIL_TYPE_MAP = {
    0: "Not Set",
    1: "Clay Loam",
    2: "Silty Clay",
    3: "Clay",
    4: "Loam",
    5: "Sandy Loam",
    6: "Loamy Sand",
    7: "Sand",
    8: "Sandy Clay",
    9: "Silt Loam",
    10: "Silt",
    99: "Other",
}

SLOPE_TYPE_MAP = {
    0: "Not Set",
    1: "Flat",
    2: "Moderate",
    3: "High",
    4: "Very High",
    99: "Other",
}

SPRINKLER_TYPE_MAP = {
    0: "Not Set",
    1: "Popup Spray",
    2: "Rotors Low Rate",
    3: "Surface Drip",
    4: "Bubblers Drip",
    5: "Rotors High Rate",
    99: "Other",
}

SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Shade"}

VEGETATION_MAP = {
    0: "Not Set",
    1: "Not Set",
    2: "Cool Season Grass",
    3: "Fruit Trees",
    4: "Flowers",
    5: "Vegetables",
    6: "Citrus",
    7: "Bushes",
    9: "Drought Tolerant Plants",
    10: "Warm Season Grass",
    11: "Trees",
    99: "Other",
}

@dataclass
class RainMachineSwitchDescription(
    SwitchEntityDescription, RainMachineDescriptionMixinUid
):
    """Describe a RainMachine switch."""

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
    """Set up RainMachine switches based on a config entry."""
    platform = entity_platform.async_get_current_platform()

    for service_name, schema, method in (
        ("start_program", {}, "async_start_program"),
        (
            "start_zone",
            {
                vol.Optional(
                    CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN
                ): cv.positive_int
            },
            "async_start_zone",
        ),
        ("stop_program", {}, "async_stop_program"),
        ("stop_zone", {}, "async_stop_zone"),
    ):
        platform.async_register_entity_service(service_name, schema, method)

    data = hass.data[DOMAIN][entry.entry_id]
    controller = data[DATA_CONTROLLER]
    program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS]
    zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES]

    entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = []

    for kind, coordinator, switch_class, switch_enabled_class in (
        ("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled),
        ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled),
    ):
        for uid, data in coordinator.data.items():
            # Add a switch to start/stop the program or zone:
            entities.append(
                switch_class(
                    entry,
                    coordinator,
                    controller,
                    RainMachineSwitchDescription(
                        key=f"{kind}_{uid}",
                        name=data["name"],
                        icon="mdi:water",
                        uid=uid,
                    ),
                )
            )

            # Add a switch to enabled/disable the program or zone:
            entities.append(
                switch_enabled_class(
                    entry,
                    coordinator,
                    controller,
                    RainMachineSwitchDescription(
                        key=f"{kind}_{uid}_enabled",
                        name=f"{data['name']} Enabled",
                        entity_category=EntityCategory.CONFIG,
                        icon="mdi:cog",
                        uid=uid,
                    ),
                )
            )

    async_add_entities(entities)

class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity):
    """Define a base RainMachine switch."""

    entity_description: RainMachineSwitchDescription

    def __init__(
        self,
        entry: ConfigEntry,
        coordinator: DataUpdateCoordinator,
        controller: Controller,
        description: RainMachineSwitchDescription,
    ) -> None:
        """Initialize."""
        super().__init__(entry, coordinator, controller, description)

        self._attr_is_on = False
        self._entry = entry

    async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None:
        """Await an API coroutine, handle any errors, and update as appropriate."""
        try:
            resp = await api_coro
        except RequestError as err:
            raise HomeAssistantError(
                f'Error while executing {api_coro.__name__} on "{self.name}": {err}',
            ) from err

        if resp["statusCode"] != 0:
            raise HomeAssistantError(
                f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}',
            )

        # Because of how inextricably linked programs and zones are, anytime one is
        # toggled, we make sure to update the data of both coordinators:
        self.hass.async_create_task(
            async_update_programs_and_zones(self.hass, self._entry)
        )

    async def async_start_program(self) -> None:
        """Execute the start_program entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_start_zone(self, *, zone_run_time: int) -> None:
        """Execute the start_zone entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_stop_program(self) -> None:
        """Execute the stop_program entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_stop_zone(self) -> None:
        """Execute the stop_zone entity service."""
        raise NotImplementedError("Service not implemented for this entity")

class RainMachineActivitySwitch(RainMachineBaseSwitch):
    """Define a RainMachine switch to start/stop an activity (program or zone)."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the switch off.

        The only way this could occur is if someone rapidly turns a disabled activity
        off right after turning it on.
        """
        if not self.coordinator.data[self.entity_description.uid]["active"]:
            raise HomeAssistantError(
                f"Cannot turn off an inactive program/zone: {self.name}"
            )

        await self.async_turn_off_when_active(**kwargs)

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        raise NotImplementedError

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the switch on."""
        if not self.coordinator.data[self.entity_description.uid]["active"]:
            self._attr_is_on = False
            self.async_write_ha_state()
            raise HomeAssistantError(
                f"Cannot turn on an inactive program/zone: {self.name}"
            )

        await self.async_turn_on_when_active(**kwargs)

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        raise NotImplementedError

class RainMachineEnabledSwitch(RainMachineBaseSwitch):
    """Define a RainMachine switch to enable/disable an activity (program or zone)."""

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        self._attr_is_on = self.coordinator.data[self.entity_description.uid]["active"]

class RainMachineProgram(RainMachineActivitySwitch):
    """Define a RainMachine program."""

    async def async_start_program(self) -> None:
        """Start the program."""
        await self.async_turn_on()

    async def async_stop_program(self) -> None:
        """Stop the program."""
        await self.async_turn_off()

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.programs.stop(self.entity_description.uid)
        )

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.programs.start(self.entity_description.uid)
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        data = self.coordinator.data[self.entity_description.uid]

        self._attr_is_on = bool(data["status"])

        next_run: str | None
        if data.get("nextRun") is None:
            next_run = None
        else:
            next_run = datetime.strptime(
                f"{data['nextRun']} {data['startTime']}",
                "%Y-%m-%d %H:%M",
            ).isoformat()

        self._attr_extra_state_attributes.update(
            {
                ATTR_ID: self.entity_description.uid,
                ATTR_NEXT_RUN: next_run,
                ATTR_SOAK: data.get("soak"),
                ATTR_STATUS: RUN_STATE_MAP[data["status"]],
                ATTR_ZONES: [z for z in data["wateringTimes"] if z["active"]],
            }
        )

class RainMachineProgramEnabled(RainMachineEnabledSwitch):
    """Define a switch to enable/disable a RainMachine program."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Disable the program."""
        tasks = [
            self._async_run_api_coroutine(
                self._controller.programs.stop(self.entity_description.uid)
            ),
            self._async_run_api_coroutine(
                self._controller.programs.disable(self.entity_description.uid)
            ),
        ]

        await asyncio.gather(*tasks)

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Enable the program."""
        await self._async_run_api_coroutine(
            self._controller.programs.enable(self.entity_description.uid)
        )

class RainMachineZone(RainMachineActivitySwitch):
    """Define a RainMachine zone."""

    async def async_start_zone(self, *, zone_run_time: int) -> None:
        """Start a particular zone for a certain amount of time."""
        await self.async_turn_on(duration=zone_run_time)

    async def async_stop_zone(self) -> None:
        """Stop a zone."""
        await self.async_turn_off()

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.zones.stop(self.entity_description.uid)
        )

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.zones.start(
                self.entity_description.uid,
                kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]),
            )
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        data = self.coordinator.data[self.entity_description.uid]

        self._attr_is_on = bool(data["state"])

        attrs = {
            ATTR_CURRENT_CYCLE: data["cycle"],
            ATTR_ID: data["uid"],
            ATTR_NO_CYCLES: data["noOfCycles"],
            ATTR_RESTRICTIONS: data("restriction"),
            ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99),
            ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99),
            ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99),
            ATTR_STATUS: RUN_STATE_MAP[data["state"]],
            ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")),
            ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99),
        }

        if "waterSense" in data:
            if "area" in data["waterSense"]:
                attrs[ATTR_AREA] = round(data["waterSense"]["area"], 2)
            if "fieldCapacity" in data["waterSense"]:
                attrs[ATTR_FIELD_CAPACITY] = round(
                    data["waterSense"]["fieldCapacity"], 2
                )
            if "precipitationRate" in data["waterSense"]:
                attrs[ATTR_PRECIP_RATE] = round(
                    data["waterSense"]["precipitationRate"], 2
                )

        self._attr_extra_state_attributes.update(attrs)

class RainMachineZoneEnabled(RainMachineEnabledSwitch):
    """Define a switch to enable/disable a RainMachine zone."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Disable the zone."""
        tasks = [
            self._async_run_api_coroutine(
                self._controller.zones.stop(self.entity_description.uid)
            ),
            self._async_run_api_coroutine(
                self._controller.zones.disable(self.entity_description.uid)
            ),
        ]

        await asyncio.gather(*tasks)

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Enable the zone."""
        await self._async_run_api_coroutine(
            self._controller.zones.enable(self.entity_description.uid)
        )
<commit_msg>Fix invalid RainMachine syntax (#72732)<commit_after>"""This component provides support for RainMachine programs and zones."""
from __future__ import annotations

import asyncio
from collections.abc import Coroutine
from dataclasses import dataclass
from datetime import datetime
from typing import Any

from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError
import voluptuous as vol

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from . import RainMachineEntity, async_update_programs_and_zones
from .const import (
    CONF_ZONE_RUN_TIME,
    DATA_CONTROLLER,
    DATA_COORDINATOR,
    DATA_PROGRAMS,
    DATA_ZONES,
    DEFAULT_ZONE_RUN,
    DOMAIN,
    RUN_STATE_MAP,
)
from .model import RainMachineDescriptionMixinUid

ATTR_AREA = "area"
ATTR_CS_ON = "cs_on"
ATTR_CURRENT_CYCLE = "current_cycle"
ATTR_CYCLES = "cycles"
ATTR_DELAY = "delay"
ATTR_DELAY_ON = "delay_on"
ATTR_FIELD_CAPACITY = "field_capacity"
ATTR_NEXT_RUN = "next_run"
ATTR_NO_CYCLES = "number_of_cycles"
ATTR_PRECIP_RATE = "sprinkler_head_precipitation_rate"
ATTR_RESTRICTIONS = "restrictions"
ATTR_SLOPE = "slope"
ATTR_SOAK = "soak"
ATTR_SOIL_TYPE = "soil_type"
ATTR_SPRINKLER_TYPE = "sprinkler_head_type"
ATTR_STATUS = "status"
ATTR_SUN_EXPOSURE = "sun_exposure"
ATTR_VEGETATION_TYPE = "vegetation_type"
ATTR_ZONES = "zones"

DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

SOIL_TYPE_MAP = {
    0: "Not Set",
    1: "Clay Loam",
    2: "Silty Clay",
    3: "Clay",
    4: "Loam",
    5: "Sandy Loam",
    6: "Loamy Sand",
    7: "Sand",
    8: "Sandy Clay",
    9: "Silt Loam",
    10: "Silt",
    99: "Other",
}

SLOPE_TYPE_MAP = {
    0: "Not Set",
    1: "Flat",
    2: "Moderate",
    3: "High",
    4: "Very High",
    99: "Other",
}

SPRINKLER_TYPE_MAP = {
    0: "Not Set",
    1: "Popup Spray",
    2: "Rotors Low Rate",
    3: "Surface Drip",
    4: "Bubblers Drip",
    5: "Rotors High Rate",
    99: "Other",
}

SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Shade"}

VEGETATION_MAP = {
    0: "Not Set",
    1: "Not Set",
    2: "Cool Season Grass",
    3: "Fruit Trees",
    4: "Flowers",
    5: "Vegetables",
    6: "Citrus",
    7: "Bushes",
    9: "Drought Tolerant Plants",
    10: "Warm Season Grass",
    11: "Trees",
    99: "Other",
}

@dataclass
class RainMachineSwitchDescription(
    SwitchEntityDescription, RainMachineDescriptionMixinUid
):
    """Describe a RainMachine switch."""

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
    """Set up RainMachine switches based on a config entry."""
    platform = entity_platform.async_get_current_platform()

    for service_name, schema, method in (
        ("start_program", {}, "async_start_program"),
        (
            "start_zone",
            {
                vol.Optional(
                    CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN
                ): cv.positive_int
            },
            "async_start_zone",
        ),
        ("stop_program", {}, "async_stop_program"),
        ("stop_zone", {}, "async_stop_zone"),
    ):
        platform.async_register_entity_service(service_name, schema, method)

    data = hass.data[DOMAIN][entry.entry_id]
    controller = data[DATA_CONTROLLER]
    program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS]
    zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES]

    entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = []

    for kind, coordinator, switch_class, switch_enabled_class in (
        ("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled),
        ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled),
    ):
        for uid, data in coordinator.data.items():
            # Add a switch to start/stop the program or zone:
            entities.append(
                switch_class(
                    entry,
                    coordinator,
                    controller,
                    RainMachineSwitchDescription(
                        key=f"{kind}_{uid}",
                        name=data["name"],
                        icon="mdi:water",
                        uid=uid,
                    ),
                )
            )

            # Add a switch to enabled/disable the program or zone:
            entities.append(
                switch_enabled_class(
                    entry,
                    coordinator,
                    controller,
                    RainMachineSwitchDescription(
                        key=f"{kind}_{uid}_enabled",
                        name=f"{data['name']} Enabled",
                        entity_category=EntityCategory.CONFIG,
                        icon="mdi:cog",
                        uid=uid,
                    ),
                )
            )

    async_add_entities(entities)

class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity):
    """Define a base RainMachine switch."""

    entity_description: RainMachineSwitchDescription

    def __init__(
        self,
        entry: ConfigEntry,
        coordinator: DataUpdateCoordinator,
        controller: Controller,
        description: RainMachineSwitchDescription,
    ) -> None:
        """Initialize."""
        super().__init__(entry, coordinator, controller, description)

        self._attr_is_on = False
        self._entry = entry

    async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None:
        """Await an API coroutine, handle any errors, and update as appropriate."""
        try:
            resp = await api_coro
        except RequestError as err:
            raise HomeAssistantError(
                f'Error while executing {api_coro.__name__} on "{self.name}": {err}',
            ) from err

        if resp["statusCode"] != 0:
            raise HomeAssistantError(
                f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}',
            )

        # Because of how inextricably linked programs and zones are, anytime one is
        # toggled, we make sure to update the data of both coordinators:
        self.hass.async_create_task(
            async_update_programs_and_zones(self.hass, self._entry)
        )

    async def async_start_program(self) -> None:
        """Execute the start_program entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_start_zone(self, *, zone_run_time: int) -> None:
        """Execute the start_zone entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_stop_program(self) -> None:
        """Execute the stop_program entity service."""
        raise NotImplementedError("Service not implemented for this entity")

    async def async_stop_zone(self) -> None:
        """Execute the stop_zone entity service."""
        raise NotImplementedError("Service not implemented for this entity")

class RainMachineActivitySwitch(RainMachineBaseSwitch):
    """Define a RainMachine switch to start/stop an activity (program or zone)."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the switch off.

        The only way this could occur is if someone rapidly turns a disabled activity
        off right after turning it on.
        """
        if not self.coordinator.data[self.entity_description.uid]["active"]:
            raise HomeAssistantError(
                f"Cannot turn off an inactive program/zone: {self.name}"
            )

        await self.async_turn_off_when_active(**kwargs)

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        raise NotImplementedError

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the switch on."""
        if not self.coordinator.data[self.entity_description.uid]["active"]:
            self._attr_is_on = False
            self.async_write_ha_state()
            raise HomeAssistantError(
                f"Cannot turn on an inactive program/zone: {self.name}"
            )

        await self.async_turn_on_when_active(**kwargs)

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        raise NotImplementedError

class RainMachineEnabledSwitch(RainMachineBaseSwitch):
    """Define a RainMachine switch to enable/disable an activity (program or zone)."""

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        self._attr_is_on = self.coordinator.data[self.entity_description.uid]["active"]

class RainMachineProgram(RainMachineActivitySwitch):
    """Define a RainMachine program."""

    async def async_start_program(self) -> None:
        """Start the program."""
        await self.async_turn_on()

    async def async_stop_program(self) -> None:
        """Stop the program."""
        await self.async_turn_off()

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.programs.stop(self.entity_description.uid)
        )

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.programs.start(self.entity_description.uid)
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        data = self.coordinator.data[self.entity_description.uid]

        self._attr_is_on = bool(data["status"])

        next_run: str | None
        if data.get("nextRun") is None:
            next_run = None
        else:
            next_run = datetime.strptime(
                f"{data['nextRun']} {data['startTime']}",
                "%Y-%m-%d %H:%M",
            ).isoformat()

        self._attr_extra_state_attributes.update(
            {
                ATTR_ID: self.entity_description.uid,
                ATTR_NEXT_RUN: next_run,
                ATTR_SOAK: data.get("soak"),
                ATTR_STATUS: RUN_STATE_MAP[data["status"]],
                ATTR_ZONES: [z for z in data["wateringTimes"] if z["active"]],
            }
        )

class RainMachineProgramEnabled(RainMachineEnabledSwitch):
    """Define a switch to enable/disable a RainMachine program."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Disable the program."""
        tasks = [
            self._async_run_api_coroutine(
                self._controller.programs.stop(self.entity_description.uid)
            ),
            self._async_run_api_coroutine(
                self._controller.programs.disable(self.entity_description.uid)
            ),
        ]

        await asyncio.gather(*tasks)

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Enable the program."""
        await self._async_run_api_coroutine(
            self._controller.programs.enable(self.entity_description.uid)
        )

class RainMachineZone(RainMachineActivitySwitch):
    """Define a RainMachine zone."""

    async def async_start_zone(self, *, zone_run_time: int) -> None:
        """Start a particular zone for a certain amount of time."""
        await self.async_turn_on(duration=zone_run_time)

    async def async_stop_zone(self) -> None:
        """Stop a zone."""
        await self.async_turn_off()

    async def async_turn_off_when_active(self, **kwargs: Any) -> None:
        """Turn the switch off when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.zones.stop(self.entity_description.uid)
        )

    async def async_turn_on_when_active(self, **kwargs: Any) -> None:
        """Turn the switch on when its associated activity is active."""
        await self._async_run_api_coroutine(
            self._controller.zones.start(
                self.entity_description.uid,
                kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]),
            )
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update the entity when new data is received."""
        data = self.coordinator.data[self.entity_description.uid]

        self._attr_is_on = bool(data["state"])

        attrs = {
            ATTR_CURRENT_CYCLE: data["cycle"],
            ATTR_ID: data["uid"],
            ATTR_NO_CYCLES: data["noOfCycles"],
            ATTR_RESTRICTIONS: data["restriction"],
            ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99),
            ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99),
            ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99),
            ATTR_STATUS: RUN_STATE_MAP[data["state"]],
            ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")),
            ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99),
        }

        if "waterSense" in data:
            if "area" in data["waterSense"]:
                attrs[ATTR_AREA] = round(data["waterSense"]["area"], 2)
            if "fieldCapacity" in data["waterSense"]:
                attrs[ATTR_FIELD_CAPACITY] = round(
                    data["waterSense"]["fieldCapacity"], 2
                )
            if "precipitationRate" in data["waterSense"]:
                attrs[ATTR_PRECIP_RATE] = round(
                    data["waterSense"]["precipitationRate"], 2
                )

        self._attr_extra_state_attributes.update(attrs)

class RainMachineZoneEnabled(RainMachineEnabledSwitch):
    """Define a switch to enable/disable a RainMachine zone."""

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Disable the zone."""
        tasks = [
            self._async_run_api_coroutine(
                self._controller.zones.stop(self.entity_description.uid)
            ),
            self._async_run_api_coroutine(
                self._controller.zones.disable(self.entity_description.uid)
            ),
        ]

        await asyncio.gather(*tasks)

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Enable the zone."""
        await self._async_run_api_coroutine(
            self._controller.zones.enable(self.entity_description.uid)
        )
<|endoftext|>


<commit_before>"""This component provides support for RainMachine programs and zones."""
from datetime import datetime
import logging

from regenmaschine.errors import RequestError

from homeassistant.components.switch import SwitchEntity
from homeassistant.const import ATTR_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from . import RainMachineEntity
from .const import (
    DATA_CLIENT,
    DATA_PROGRAMS,
    DATA_ZONES,
    DATA_ZONES_DETAILS,
    DOMAIN as RAINMACHINE_DOMAIN,
    PROGRAM_UPDATE_TOPIC,
    ZONE_UPDATE_TOPIC,
)

_LOGGER = logging.getLogger(__name__)

ATTR_AREA = "area"
ATTR_CS_ON = "cs_on"
ATTR_CURRENT_CYCLE = "current_cycle"
ATTR_CYCLES = "cycles"
ATTR_DELAY = "delay"
ATTR_DELAY_ON = "delay_on"
ATTR_FIELD_CAPACITY = "field_capacity"
ATTR_NEXT_RUN = "next_run"
ATTR_NO_CYCLES = "number_of_cycles"
ATTR_PRECIP_RATE = "sprinkler_head_precipitation_rate"
ATTR_RESTRICTIONS = "restrictions"
ATTR_SLOPE = "slope"
ATTR_SOAK = "soak"
ATTR_SOIL_TYPE = "soil_type"
ATTR_SPRINKLER_TYPE = "sprinkler_head_type"
ATTR_STATUS = "status"
ATTR_SUN_EXPOSURE = "sun_exposure"
ATTR_TIME_REMAINING = "time_remaining"
ATTR_VEGETATION_TYPE = "vegetation_type"
ATTR_ZONES = "zones"

DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

PROGRAM_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"}

SOIL_TYPE_MAP = {
    0: "Not Set",
    1: "Clay Loam",
    2: "Silty Clay",
    3: "Clay",
    4: "Loam",
    5: "Sandy Loam",
    6: "Loamy Sand",
    7: "Sand",
    8: "Sandy Clay",
    9: "Silt Loam",
    10: "Silt",
    99: "Other",
}

SLOPE_TYPE_MAP = {
    0: "Not Set",
    1: "Flat",
    2: "Moderate",
    3: "High",
    4: "Very High",
    99: "Other",
}

SPRINKLER_TYPE_MAP = {
    0: "Not Set",
    1: "Popup Spray",
    2: "Rotors",
    3: "Surface Drip",
    4: "Bubblers Drip",
    99: "Other",
}

SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Shade"}

VEGETATION_MAP = {
    0: "Not Set",
    2: "Cool Season Grass",
    3: "Fruit Trees",
    4: "Flowers",
    5: "Vegetables",
    6: "Citrus",
    7: "Trees and Bushes",
    9: "Drought Tolerant Plants",
    10: "Warm Season Grass",
    99: "Other",
}

SWITCH_TYPE_PROGRAM = "program"
SWITCH_TYPE_ZONE = "zone"

async def async_setup_entry(hass, entry, async_add_entities):
    """Set up RainMachine switches based on a config entry."""
    rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]

    entities = []
    for program in rainmachine.data[DATA_PROGRAMS]:
        entities.append(RainMachineProgram(rainmachine, program))
    for zone in rainmachine.data[DATA_ZONES]:
        entities.append(RainMachineZone(rainmachine, zone))

    async_add_entities(entities, True)

class RainMachineSwitch(RainMachineEntity, SwitchEntity):
    """A class to represent a generic RainMachine switch."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a generic RainMachine switch."""
        super().__init__(rainmachine)

        self._is_on = False
        self._name = switch_data["name"]
        self._switch_data = switch_data
        self._rainmachine_entity_id = switch_data["uid"]
        self._switch_type = None

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        return self._switch_data["active"]

    @property
    def icon(self) -> str:
        """Return the icon."""
        return "mdi:water"

    @property
    def is_on(self) -> bool:
        """Return whether the program is running."""
        return self._is_on

    @property
    def unique_id(self) -> str:
        """Return a unique, Home Assistant friendly identifier for this entity."""
        return "{}_{}_{}".format(
            self.rainmachine.device_mac.replace(":", ""),
            self._switch_type,
            self._rainmachine_entity_id,
        )

    async def _async_run_switch_coroutine(self, api_coro) -> None:
        """Run a coroutine to toggle the switch."""
        try:
            resp = await api_coro
        except RequestError as err:
            _LOGGER.error(
                'Error while toggling %s "%s": %s',
                self._switch_type,
                self.unique_id,
                err,
            )
            return

        if resp["statusCode"] != 0:
            _LOGGER.error(
                'Error while toggling %s "%s": %s',
                self._switch_type,
                self.unique_id,
                resp["message"],
            )
            return

        self.hass.async_create_task(self.rainmachine.async_update_programs_and_zones())

class RainMachineProgram(RainMachineSwitch):
    """A RainMachine program."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a generic RainMachine switch."""
        super().__init__(rainmachine, switch_data)
        self._switch_type = SWITCH_TYPE_PROGRAM

    @property
    def zones(self) -> list:
        """Return a list of active zones associated with this program."""
        return [z for z in self._switch_data["wateringTimes"] if z["active"]]

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
            )
        )

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the program off."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.programs.stop(self._rainmachine_entity_id)
        )

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the program on."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.programs.start(self._rainmachine_entity_id)
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update info for the program."""
        [self._switch_data] = [
            p
            for p in self.rainmachine.data[DATA_PROGRAMS]
            if p["uid"] == self._rainmachine_entity_id
        ]

        self._is_on = bool(self._switch_data["status"])

        try:
            next_run = datetime.strptime(
                "{} {}".format(
                    self._switch_data["nextRun"], self._switch_data["startTime"]
                ),
                "%Y-%m-%d %H:%M",
            ).isoformat()
        except ValueError:
            next_run = None

        self._attrs.update(
            {
                ATTR_ID: self._switch_data["uid"],
                ATTR_NEXT_RUN: next_run,
                ATTR_SOAK: self._switch_data.get("soak"),
                ATTR_STATUS: PROGRAM_STATUS_MAP[self._switch_data["status"]],
                ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
            }
        )

class RainMachineZone(RainMachineSwitch):
    """A RainMachine zone."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a RainMachine zone."""
        super().__init__(rainmachine, switch_data)
        self._switch_type = SWITCH_TYPE_ZONE

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
            )
        )
        self.async_on_remove(
            async_dispatcher_connect(self.hass, ZONE_UPDATE_TOPIC, self._update_state)
        )

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the zone off."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.zones.stop(self._rainmachine_entity_id)
        )

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the zone on."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.zones.start(
                self._rainmachine_entity_id, self.rainmachine.default_zone_runtime
            )
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update info for the zone."""
        [self._switch_data] = [
            z
            for z in self.rainmachine.data[DATA_ZONES]
            if z["uid"] == self._rainmachine_entity_id
        ]
        [details] = [
            z
            for z in self.rainmachine.data[DATA_ZONES_DETAILS]
            if z["uid"] == self._rainmachine_entity_id
        ]

        self._is_on = bool(self._switch_data["state"])

        self._attrs.update(
            {
                ATTR_AREA: details.get("waterSense").get("area"),
                ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"),
                ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"),
                ATTR_ID: self._switch_data["uid"],
                ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"),
                ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"),
                ATTR_RESTRICTIONS: self._switch_data.get("restriction"),
                ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")),
                ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")),
                ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")),
                ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")),
                ATTR_TIME_REMAINING: self._switch_data.get("remaining"),
                ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")),
            }
        )
<commit_msg>add zone status attribute so we can know if queued (#39133)<commit_after>"""This component provides support for RainMachine programs and zones."""
from datetime import datetime
import logging

from regenmaschine.errors import RequestError

from homeassistant.components.switch import SwitchEntity
from homeassistant.const import ATTR_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from . import RainMachineEntity
from .const import (
    DATA_CLIENT,
    DATA_PROGRAMS,
    DATA_ZONES,
    DATA_ZONES_DETAILS,
    DOMAIN as RAINMACHINE_DOMAIN,
    PROGRAM_UPDATE_TOPIC,
    ZONE_UPDATE_TOPIC,
)

_LOGGER = logging.getLogger(__name__)

ATTR_AREA = "area"
ATTR_CS_ON = "cs_on"
ATTR_CURRENT_CYCLE = "current_cycle"
ATTR_CYCLES = "cycles"
ATTR_DELAY = "delay"
ATTR_DELAY_ON = "delay_on"
ATTR_FIELD_CAPACITY = "field_capacity"
ATTR_NEXT_RUN = "next_run"
ATTR_NO_CYCLES = "number_of_cycles"
ATTR_PRECIP_RATE = "sprinkler_head_precipitation_rate"
ATTR_RESTRICTIONS = "restrictions"
ATTR_SLOPE = "slope"
ATTR_SOAK = "soak"
ATTR_SOIL_TYPE = "soil_type"
ATTR_SPRINKLER_TYPE = "sprinkler_head_type"
ATTR_STATUS = "status"
ATTR_SUN_EXPOSURE = "sun_exposure"
ATTR_TIME_REMAINING = "time_remaining"
ATTR_VEGETATION_TYPE = "vegetation_type"
ATTR_ZONES = "zones"

DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"}

SOIL_TYPE_MAP = {
    0: "Not Set",
    1: "Clay Loam",
    2: "Silty Clay",
    3: "Clay",
    4: "Loam",
    5: "Sandy Loam",
    6: "Loamy Sand",
    7: "Sand",
    8: "Sandy Clay",
    9: "Silt Loam",
    10: "Silt",
    99: "Other",
}

SLOPE_TYPE_MAP = {
    0: "Not Set",
    1: "Flat",
    2: "Moderate",
    3: "High",
    4: "Very High",
    99: "Other",
}

SPRINKLER_TYPE_MAP = {
    0: "Not Set",
    1: "Popup Spray",
    2: "Rotors",
    3: "Surface Drip",
    4: "Bubblers Drip",
    99: "Other",
}

SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Shade"}

VEGETATION_MAP = {
    0: "Not Set",
    2: "Cool Season Grass",
    3: "Fruit Trees",
    4: "Flowers",
    5: "Vegetables",
    6: "Citrus",
    7: "Trees and Bushes",
    9: "Drought Tolerant Plants",
    10: "Warm Season Grass",
    99: "Other",
}

SWITCH_TYPE_PROGRAM = "program"
SWITCH_TYPE_ZONE = "zone"

async def async_setup_entry(hass, entry, async_add_entities):
    """Set up RainMachine switches based on a config entry."""
    rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]

    entities = []
    for program in rainmachine.data[DATA_PROGRAMS]:
        entities.append(RainMachineProgram(rainmachine, program))
    for zone in rainmachine.data[DATA_ZONES]:
        entities.append(RainMachineZone(rainmachine, zone))

    async_add_entities(entities, True)

class RainMachineSwitch(RainMachineEntity, SwitchEntity):
    """A class to represent a generic RainMachine switch."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a generic RainMachine switch."""
        super().__init__(rainmachine)

        self._is_on = False
        self._name = switch_data["name"]
        self._switch_data = switch_data
        self._rainmachine_entity_id = switch_data["uid"]
        self._switch_type = None

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        return self._switch_data["active"]

    @property
    def icon(self) -> str:
        """Return the icon."""
        return "mdi:water"

    @property
    def is_on(self) -> bool:
        """Return whether the program is running."""
        return self._is_on

    @property
    def unique_id(self) -> str:
        """Return a unique, Home Assistant friendly identifier for this entity."""
        return "{}_{}_{}".format(
            self.rainmachine.device_mac.replace(":", ""),
            self._switch_type,
            self._rainmachine_entity_id,
        )

    async def _async_run_switch_coroutine(self, api_coro) -> None:
        """Run a coroutine to toggle the switch."""
        try:
            resp = await api_coro
        except RequestError as err:
            _LOGGER.error(
                'Error while toggling %s "%s": %s',
                self._switch_type,
                self.unique_id,
                err,
            )
            return

        if resp["statusCode"] != 0:
            _LOGGER.error(
                'Error while toggling %s "%s": %s',
                self._switch_type,
                self.unique_id,
                resp["message"],
            )
            return

        self.hass.async_create_task(self.rainmachine.async_update_programs_and_zones())

class RainMachineProgram(RainMachineSwitch):
    """A RainMachine program."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a generic RainMachine switch."""
        super().__init__(rainmachine, switch_data)
        self._switch_type = SWITCH_TYPE_PROGRAM

    @property
    def zones(self) -> list:
        """Return a list of active zones associated with this program."""
        return [z for z in self._switch_data["wateringTimes"] if z["active"]]

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
            )
        )

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the program off."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.programs.stop(self._rainmachine_entity_id)
        )

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the program on."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.programs.start(self._rainmachine_entity_id)
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update info for the program."""
        [self._switch_data] = [
            p
            for p in self.rainmachine.data[DATA_PROGRAMS]
            if p["uid"] == self._rainmachine_entity_id
        ]

        self._is_on = bool(self._switch_data["status"])

        try:
            next_run = datetime.strptime(
                "{} {}".format(
                    self._switch_data["nextRun"], self._switch_data["startTime"]
                ),
                "%Y-%m-%d %H:%M",
            ).isoformat()
        except ValueError:
            next_run = None

        self._attrs.update(
            {
                ATTR_ID: self._switch_data["uid"],
                ATTR_NEXT_RUN: next_run,
                ATTR_SOAK: self._switch_data.get("soak"),
                ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["status"]],
                ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
            }
        )

class RainMachineZone(RainMachineSwitch):
    """A RainMachine zone."""

    def __init__(self, rainmachine, switch_data):
        """Initialize a RainMachine zone."""
        super().__init__(rainmachine, switch_data)
        self._switch_type = SWITCH_TYPE_ZONE

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
            )
        )
        self.async_on_remove(
            async_dispatcher_connect(self.hass, ZONE_UPDATE_TOPIC, self._update_state)
        )

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the zone off."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.zones.stop(self._rainmachine_entity_id)
        )

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the zone on."""
        await self._async_run_switch_coroutine(
            self.rainmachine.controller.zones.start(
                self._rainmachine_entity_id, self.rainmachine.default_zone_runtime
            )
        )

    @callback
    def update_from_latest_data(self) -> None:
        """Update info for the zone."""
        [self._switch_data] = [
            z
            for z in self.rainmachine.data[DATA_ZONES]
            if z["uid"] == self._rainmachine_entity_id
        ]
        [details] = [
            z
            for z in self.rainmachine.data[DATA_ZONES_DETAILS]
            if z["uid"] == self._rainmachine_entity_id
        ]

        self._is_on = bool(self._switch_data["state"])

        self._attrs.update(
            {
                ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["state"]],
                ATTR_AREA: details.get("waterSense").get("area"),
                ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"),
                ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"),
                ATTR_ID: self._switch_data["uid"],
                ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"),
                ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"),
                ATTR_RESTRICTIONS: self._switch_data.get("restriction"),
                ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")),
                ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")),
                ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")),
                ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")),
                ATTR_TIME_REMAINING: self._switch_data.get("remaining"),
                ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")),
            }
        )
<|endoftext|>


<commit_before>"""
homeassistant.components.scheduler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component that will act as a scheduler and performe actions based
on the events in the schedule.

It will read a json object from schedule.json in the config dir
and create a schedule based on it.
Each schedule is a JSON with the keys id, name, description,
entity_ids, and events.
- days is an array with the weekday number (monday=0) that the schdule
    is active
- entity_ids an array with entity ids that the events in the schedule should
    effect (can also be groups)
- events is an array of objects that describe the different events that is
    supported. Read in the events descriptions for more information
"""
import logging
import json

from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID

# The domain of your component. Should be equal to the name of your component
DOMAIN = 'scheduler'

DEPENDENCIES = []

_LOGGER = logging.getLogger(__name__)

_SCHEDULE_FILE = 'schedule.json'

def setup(hass, config):
    """ Create the schedules """

    def setup_listener(schedule, event_data):
        """ Creates the event listener based on event_data """
        event_type = event_data['type']
        component = event_type

        # if the event isn't part of a component
        if event_type in ['time']:
            component = 'scheduler.{}'.format(event_type)

        elif not bootstrap.setup_component(hass, component, config):
            _LOGGER.warn("Could setup event listener for %s", component)
            return None

        return get_component(component).create_event_listener(schedule,
                                                              event_data)

    def setup_schedule(schedule_data):
        """ setup a schedule based on the description """

        schedule = Schedule(schedule_data['id'],
                            name=schedule_data['name'],
                            description=schedule_data['description'],
                            entity_ids=schedule_data['entity_ids'],
                            days=schedule_data['days'])

        for event_data in schedule_data['events']:
            event_listener = setup_listener(schedule, event_data)

            if event_listener:
                schedule.add_event_listener(event_listener)

        schedule.schedule(hass)
        return True

    with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file:
        schedule_descriptions = json.load(schedule_file)

    for schedule_description in schedule_descriptions:
        if not setup_schedule(schedule_description):
            return False

    return True

class Schedule(object):
    """ A Schedule """

    # pylint: disable=too-many-arguments
    def __init__(self, schedule_id, name=None, description=None,
                 entity_ids=None, days=None):

        self.schedule_id = schedule_id
        self.name = name
        self.description = description

        self.entity_ids = entity_ids or []

        self.days = days or [0, 1, 2, 3, 4, 5, 6]

        self.__event_listeners = []

    def add_event_listener(self, event_listener):
        """ Add a event to the schedule """
        self.__event_listeners.append(event_listener)

    def schedule(self, hass):
        """ Schedule all the events in the schdule """
        for event in self.__event_listeners:
            event.schedule(hass)

class EventListener(object):
    """ The base EventListner class that the schedule uses """
    def __init__(self, schedule):
        self.my_schedule = schedule

    def schedule(self, hass):
        """ Schedule the event """
        pass

    def execute(self, hass):
        """ execute the event """
        pass

# pylint: disable=too-few-public-methods
class ServiceEventListener(EventListener):
    """ A EventListner that calls a service when executed """

    def __init__(self, schdule, service):
        EventListener.__init__(self, schdule)

        (self.domain, self.service) = service.split('.')

    def execute(self, hass):
        """ Call the service """
        data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
        hass.call_service(self.domain, self.service, data)

        # Reschedule for next day
        self.schedule(hass)
<commit_msg>fix typo<commit_after>"""
homeassistant.components.scheduler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A component that will act as a scheduler and perform actions based
on the events in the schedule.

It will read a json object from schedule.json in the config dir
and create a schedule based on it.
Each schedule is a JSON with the keys id, name, description,
entity_ids, and events.
- days is an array with the weekday number (monday=0) that the schdule
    is active
- entity_ids an array with entity ids that the events in the schedule should
    effect (can also be groups)
- events is an array of objects that describe the different events that is
    supported. Read in the events descriptions for more information
"""
import logging
import json

from homeassistant import bootstrap
from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID

# The domain of your component. Should be equal to the name of your component
DOMAIN = 'scheduler'

DEPENDENCIES = []

_LOGGER = logging.getLogger(__name__)

_SCHEDULE_FILE = 'schedule.json'

def setup(hass, config):
    """ Create the schedules """

    def setup_listener(schedule, event_data):
        """ Creates the event listener based on event_data """
        event_type = event_data['type']
        component = event_type

        # if the event isn't part of a component
        if event_type in ['time']:
            component = 'scheduler.{}'.format(event_type)

        elif not bootstrap.setup_component(hass, component, config):
            _LOGGER.warn("Could setup event listener for %s", component)
            return None

        return get_component(component).create_event_listener(schedule,
                                                              event_data)

    def setup_schedule(schedule_data):
        """ setup a schedule based on the description """

        schedule = Schedule(schedule_data['id'],
                            name=schedule_data['name'],
                            description=schedule_data['description'],
                            entity_ids=schedule_data['entity_ids'],
                            days=schedule_data['days'])

        for event_data in schedule_data['events']:
            event_listener = setup_listener(schedule, event_data)

            if event_listener:
                schedule.add_event_listener(event_listener)

        schedule.schedule(hass)
        return True

    with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file:
        schedule_descriptions = json.load(schedule_file)

    for schedule_description in schedule_descriptions:
        if not setup_schedule(schedule_description):
            return False

    return True

class Schedule(object):
    """ A Schedule """

    # pylint: disable=too-many-arguments
    def __init__(self, schedule_id, name=None, description=None,
                 entity_ids=None, days=None):

        self.schedule_id = schedule_id
        self.name = name
        self.description = description

        self.entity_ids = entity_ids or []

        self.days = days or [0, 1, 2, 3, 4, 5, 6]

        self.__event_listeners = []

    def add_event_listener(self, event_listener):
        """ Add a event to the schedule """
        self.__event_listeners.append(event_listener)

    def schedule(self, hass):
        """ Schedule all the events in the schdule """
        for event in self.__event_listeners:
            event.schedule(hass)

class EventListener(object):
    """ The base EventListner class that the schedule uses """
    def __init__(self, schedule):
        self.my_schedule = schedule

    def schedule(self, hass):
        """ Schedule the event """
        pass

    def execute(self, hass):
        """ execute the event """
        pass

# pylint: disable=too-few-public-methods
class ServiceEventListener(EventListener):
    """ A EventListner that calls a service when executed """

    def __init__(self, schdule, service):
        EventListener.__init__(self, schdule)

        (self.domain, self.service) = service.split('.')

    def execute(self, hass):
        """ Call the service """
        data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
        hass.call_service(self.domain, self.service, data)

        # Reschedule for next day
        self.schedule(hass)
<|endoftext|>