package code;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ContentProducer;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;

import anywheresoftware.b4a.BA;
import lombok.Getter;
import lombok.ToString;


@BA.Events(values= {
	"receivebuffer(fromip as string, contenttype as string, buf() as byte)"
})

/**
 * Vapix Audio Device
 * @author rdkartono
 *
 */
@BA.ShortName("VapixAudio")
public class VapixAudio extends VapixDevice{
	private Gson gs;
	public AudioApi AudioApi;
	public MediaApi MediaApi;
	public AudioRelayServiceApi AudioRelayServiceApi;
	public AudioControlServiceApi AudioControlServiceApi;
	public CallServiceApi CallServiceApi;
	
	
	private boolean need_receivebuffer_event = false;
	
	
	public VapixAudio(VapixDevice dev) {
		super(dev);
		
		if (ba_inited) {
			need_receivebuffer_event = ba.subExists(event+"_receivebuffer");
		}
		
		Me = this;
		gs = new Gson();
		AudioApi = new AudioApi();
		MediaApi = new MediaApi();
		AudioRelayServiceApi = new AudioRelayServiceApi();
		AudioControlServiceApi = new AudioControlServiceApi();
		CallServiceApi = new CallServiceApi();
	}
	
	
	private void raise_receivebuffer(String fromip, String contenttype, byte[] buf) {
		if (need_receivebuffer_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, event+"_receivebuffer", false, new Object[] {fromip, contenttype, buf});
		}
	}
	
	/**
	 * Class to group Methods for Audio API
	 * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10036015/display
	 * @author rdkartono
	 *
	 */
	public class AudioApi{
		
		// Buffer used for Start_Receive thread
		private ByteBuffer rxbuf = ByteBuffer.allocate(0);
		private @Getter Boolean ReceiveRunning = false;
		
		// Buffer used for Start_Transmit thread
		private ByteBuffer txbuf = ByteBuffer.allocate(0);
		private @Getter Boolean TransmitRunning = false;
		
		public boolean IsSupported() {
			String ver = GetParameterValue("Properties.API.HTTP.Version");
			String yes1 = GetParameterValue("Properties.Audio.Audio");
			if ("3".equals(ver)) {
				if ("yes".equals(yes1)) {
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Check if have audio input on channel
		 * @param channel start from 0
		 * @return true if have
		 */
		public boolean HasAudioInput(int channel) {
			return "yes".equals(GetParameterValue("Properties.Audio.Source.A"+channel+".Input"));
			
//			String result = GetParameterValue("Properties.Audio.Source.A"+channel+".Input");
//			if (common.ValidString(result)) {
//				return result.equals("yes");
//			}
//			return false;
		}
		
		/**
		 * Check if have audio output on channel
		 * @param channel start from 0
		 * @return true if have
		 */
		public boolean HasAudioOutput(int channel) {
			return "yes".equals(GetParameterValue("Properties.Audio.Source.A"+channel+".Input"));
			
//			String result = c;
//			if (common.ValidString(result)) {
//				return result.equals("yes");
//			}
//			return false;
		}
		
		/**
		 * Check Supported Encoding Format
		 * @return empty String array if failed
		 */
		public String[] EncodingFormat() {
			String result = GetParameterValue("Properties.Audio.Format");
			if (common.ValidString(result)) {
				return result.split(",");
			} else return new String[0];
		}
		
		/**
		 * Check Supported Decoding Format
		 * @return empty String array if failed
		 */
		public String[] DecodingFormat() {
			String result = GetParameterValue("Properties.Audio.Decoder.Format");
			if (common.ValidString(result)) {
				return result.split(",");
			} else return new String[0];
		}
		
		/**
		 * Check Supported Input Level Type
		 * typical result = mic, line
		 * @return empty String array if failed
		 */
		public String[] InputType() {
			String result = GetParameterValue("Properties.Audio.InputType");
			if (common.ValidString(result)) {
				return result.split(",");
			} else return new String[0];
		}
		
		/**
		 * Check supported Duplex Mode
		 * Typical result = full, half, post, get
		 * @return empty String array if failed
		 */
		public String[] DuplexMode() {
			String result = GetParameterValue("Properties.Audio.DuplexMode");
			if (common.ValidString(result)) {
				return result.split(",");
			} else return new String[0];
		}
	
		/**
		 * Check if Audio Support is enabled on audio channel
		 * @param ID start from 0
		 * @return true if audio function is supported
		 */
		public boolean GetAudioSupport(int ID) {
			return "yes".equals(GetParameterValue("AudioSource.A"+ID+".AudioSupport"));
			
//			String val = GetParameterValue("AudioSource.A"+ID+".AudioSupport");
//			if (common.ValidString(val)) {
//				return val.equals("yes");
//			}
//			return false;
		}
		
		/**
		 * Change Audio Support status on audio channel
		 * @param ID start from 0
		 * @param yes if true, enable audio support on ID
		 * @return true if can be set
		 */
		public boolean ChangeAudioSupport(int ID, boolean yes) {
			int code = HTTPGet_NoResult("/axis-cgi/param.cgi","action=update",MessageFormat.format("AudioSource.A{0}.AudioSupport={1}", ID, (yes ? "yes":"no")));
			return code==200;
		}
		
		/**
		 * Get Audio Enabled on audio channel
		 * @param ID start from 0
		 * @return true if enabled
		 */
		public boolean GetAudioEnabled(int ID) {
			return "yes".equals(GetParameterValue("Audio.A"+ID+".Enabled"));
			
//			String val = GetParameterValue("Audio.A"+ID+".Enabled");
//			if (common.ValidString(val)) {
//				return val.equals("yes");
//			}
//			return false;
		}
		
		/**
		 * Change Audio Enabled on audio channel
		 * @param ID start from 0
		 * @param yes if true, enable audio 
		 * @return true if can be set
		 */
		public boolean ChangeAudioEnabled(int ID, boolean yes) {
			int code = HTTPGet_NoResult("/axis-cgi/param.cgi","action=update",MessageFormat.format("Audio.A{0}.Enabled={1}", ID, (yes ? "yes":"no")));
			return code==200;
		}
		
		/**
		 * Check available bytes to read from ReceiveBuffer
		 * @return 0 if empty
		 */
		public int AvailableReceivedBytes() {
			if (common.ByteBufferReady(rxbuf)) {
				if (common.IsReadMode(rxbuf)) {
					return rxbuf.remaining();
				} else {
					return rxbuf.position();
				}
			}
			return 0;
		}
		
		/**
		 * Get All available bytes from Receive Buffer
		 * @return null if failed or not available
		 */
//		public byte[] GetReceivedBuffers() {
//				if (common.ByteBufferReady(rxbuf)) {
//					synchronized(rxbuf) {
//						// berubah jadi read mode kalau perlu
//						if (common.ChangeToReadMode(rxbuf)) {
//							int len = common.AvailableToRead(rxbuf);
//							if (len>0) {
//								byte[] buf = new byte[len];
//								rxbuf.get(buf);
//								common.ChangeToWriteMode(rxbuf);
//								return buf;
//							}
//						}
//					}
//				}
//			return null;
//		}
		
		/**
		 * Push some bytes to Transmit Buffer
		 * @param buf buffer to push
		 * @return true if can be pushed
		 */
		public boolean PutTransmitBuffer(byte[] buf) {
			if (buf!=null && buf.length>0) {
				int len = buf.length + txbuf.capacity();
				ByteBuffer temp = ByteBuffer.allocate(len);
				temp.put(txbuf);
				temp.put(buf);
				
				txbuf = temp;
				return true;
			}
			return false;
		}
		
		/**
		 * Get available bytes from Receive Buffer, by some limit
		 * @param maxsize how many bytes to read, give 0 to read all
		 * @return null if failed or not available
		 */
		public byte[] GetReceivedBuffers_bySize(final int maxsize) {
			
			if (common.ByteBufferReady(rxbuf)) {
				synchronized(rxbuf) {
					if (rxbuf.position()>0) {
						// sudah ada yang ketulis
						rxbuf.flip();	// pindah mode baca
						int len = rxbuf.remaining();
						if (maxsize>0) {
							if (len>maxsize) len = maxsize;
						}
						byte[] buf = new byte[len];
						rxbuf.get(buf);
						rxbuf.compact();	// pindah mode tulis
						return buf;
					}
				}
			}
			return null;
		}
		
		/**
		 * Start Receive
		 * Receive : tell VapixAudio to start giving streaming bytes to Java app
		 * Java App use GetReceiveBuffers function to get data from VapixAudio
		 * @return true if can be started
		 */
		public boolean Start_Receive() {
			//TODO belum ditest
			Stop_Receive();
			CloseableHttpResponse resp = client.HttpGet("/axis-cgi/audio/receive.cgi", "httptype=singlepart");
			if (resp!=null) {
				int code = client.GetStatusCode(resp);
				String msg = client.GetStatusMessage(resp);
				if (code==200) {
					Thread thread = new Thread(()->{
						String contenttype = resp.getEntity().getContentType().getValue();
						long contentlength = resp.getEntity().getContentLength();
						try (InputStream inp = resp.getEntity().getContent()) {
							
							ReceiveRunning = true;
							raise_log(MessageFormat.format("Start_Receive started, Content-Type={0}, Content-Length={1}", contenttype, contentlength));
							while(ReceiveRunning) {
								byte[] xx = new byte[1024];
								int len;
								
								try {
									
									len = inp.read(xx);
								} catch (IOException e) {
									raise_log("ReceiverRunner IOException = "+e.getMessage());
									Stop_Receive();
									len = 0;
								}
								if (len>0) {
									
									if (len!=xx.length) {
										byte[] yy = Arrays.copyOf(xx, len);
										xx = yy;
									}
									//raise_log("ReceiveRunner got "+len+" bytes");
									raise_receivebuffer(client.target.getHostName(),contenttype,xx);
									
									ByteBuffer temp = ByteBuffer.allocate(rxbuf.capacity()+len);
									temp.put(rxbuf);
									temp.put(xx);
									rxbuf = temp;
									
									//raise_log("ReceiveRunner got "+len+" bytes, rxbuf capacity = "+rxbuf.capacity());
								}
							}
							raise_log("Start_Receive stopped");
							resp.close();
						} catch (UnsupportedOperationException | IOException e) {
							raise_log("Start_Receive exception = "+e.getMessage());
						}
						
					}) ;
					thread.start();
					return true;
				} else raise_log("Start_Receive code = "+code+", Message = "+msg);
			} else raise_log("Start_Receive HttpGet returning null");
			return false;
		}
		
		/**
		 * Stop Receive
		 */
		public void Stop_Receive() {
			ReceiveRunning = false;
			rxbuf = ByteBuffer.allocate(0);
		}
		
		/**
		 * Start Transmit
		 * Transmit : tell VapixAudio to start receiving bytes from Java app
		 * Java app use PutTransmitBuffer function to push data to VapixAudio
		 * @return true if can be started
		 */
		public boolean Start_Transmit() {
			//TODO belum dibuat
			Stop_Transmit();
			
			CloseableHttpResponse resp = client.HttpPost("audio/basic", new ContentProducer() {

				@Override
				public void writeTo(OutputStream outStream) {
					if (TransmitRunning) {
						if (txbuf!=null) {
							// masih lanjut
							synchronized(txbuf) {
								if (common.IsWriteMode(txbuf)) common.ChangeToReadMode(txbuf);
								int len = common.AvailableToRead(txbuf);
								if (len>0) {
									byte[] xx = new byte[len];
									txbuf.get(xx);
									try {
										outStream.write(xx);
										raise_log("Wrote "+len+" bytes to OutputStream ContentProducer");
									} catch (IOException e) {
										raise_log("Write OutputStream ContentProducer IOException = "+e.getMessage());
									}
								}
								common.ChangeToWriteMode(txbuf);
							}
						}
					}
					
					// finish
					try {
						outStream.close();
						raise_log("Closed OutputStream ContentProducer");
					} catch (IOException e) {
						raise_log("Closing OutputStream ContentProducer IOException = "+e.getMessage());
					}
					
				}
				
			}, "/axis-cgi/audio/transmit.cgi", "");
			
			if (resp!=null) {
				int code = client.GetStatusCode(resp);
				String msg = client.GetStatusMessage(resp);
				raise_log(MessageFormat.format("Start_Transmit code={0}, Message={1}", code, msg));
				
				if (code==200) {
					Thread thread = new Thread(()->{
						raise_log("Start_Transmit started");
						TransmitRunning = true;
						while(TransmitRunning) {
							try {
								Thread.sleep(100);
							} catch (InterruptedException e) {
								break;
							}
						}
						raise_log("Start_Transmit stopped");
					});
					thread.start();
					
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Stop Transmit
		 */
		public void Stop_Transmit() {
			TransmitRunning = false;
			txbuf = ByteBuffer.allocate(0);
		}
		
		
	}
	
	/**
	 * Class to group Methods for Media Clip API
	 * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10036045/display
	 * @author rdkartono
	 *
	 */
	public class MediaApi{
		private final String url_param = "/axis-cgi/param.cgi";
		private final String url_mediaclip = "/axis-cgi/mediaclip.cgi";
		private final String url_playclip = "/axis-cgi/playclip.cgi";
		private final String maxgroups_header = "root.MediaClip.MaxGroups=";
		private final String maxuploadsize_header = "root.MediaClip.MaxUploadSize=";
		
		/**
		 * Available Media Clips on the device
		 * Update this variable using GetAvailableMediaClips
		 */
		public MediaClip[] AvailableMediaClips;
		
		/**
		 * Maximum recognized Clip ID
		 */
		public int MediaClip_MaxGroups = 0;
		
		/**
		 * Upload Size in MB
		 */
		public int MediaClip_MaxUploadSize = 0;
		
		/**
		 * Check if Media Clip Playback is supported using specific Audio Ouput
		 * @param deviceid start from 0
		 * @return true if supported
		 */
		public boolean IsSupported(int deviceid) {
			String ver = GetParameterValue("Properties.API.HTTP.Version");
			String yes1 = GetParameterValue("Properties.Audio.Audio");
			String yes2 = GetParameterValue("Properties.Audio.Source.A0.Output");
			if ("3".equals(ver)) {
				if ("yes".equals(yes1)) {
					if ("yes".equals(yes2)) {
						return true;
					}
				}
			}
			return false;
		}
		
		/**
		 * Get Avaliable Media Clips installed at Device
		 * Result will be on variable AvailableMediaClips
		 * @return true if success
		 */
		public boolean GetAvailableMediaClips() {
			AvailableMediaClips = new MediaClip[0];
			List<String> getresult = HTTPGet(url_param, "action=list","group=MediaClip");
			if (common.ValidListOfString(getresult)) {
				Pattern pp = Pattern.compile("root.MediaClip.M(\\d+).(\\S+)=(\\S+( \\w+)*)");
				Map<Integer, MediaClip> map = new HashMap<Integer, MediaClip>();
				getresult.forEach(ss ->{
					//raise_log(ss);
					Matcher mm = pp.matcher(ss);
					if (mm.find()) {
						try {
							Integer ID = Integer.valueOf(mm.group(1));
							String parameter = mm.group(2);
							String value = mm.group(3);
							//raise_log(MessageFormat.format("ID={0}, Parameter={1}, Value={2}", ID, parameter, value));
							MediaClip xx = map.get(ID);
							if (xx==null) {
								xx = new MediaClip();
								xx.ID = ID;
								map.put(ID, xx);
							}
							// pake Reflection
							MediaClip.class.getField(parameter).set(xx, value);
							
						} catch(IllegalArgumentException | IndexOutOfBoundsException | IllegalStateException | SecurityException | IllegalAccessException | NoSuchFieldException ex) {
							raise_log("GetAvailableMediaClips failed for value="+ss+", Exception="+ex.getMessage());
						} 
						
					} else {
						if (ss.startsWith(maxgroups_header)) {
							MediaClip_MaxGroups = Integer.parseInt(ss.substring(maxgroups_header.length()));
							raise_log("MediaClip MaxGroups = "+MediaClip_MaxGroups);
						} else if (ss.startsWith(maxuploadsize_header)) {
							MediaClip_MaxUploadSize = Integer.parseInt(ss.substring(maxuploadsize_header.length()));
							raise_log("MediaClip MaxUploadSize = "+MediaClip_MaxUploadSize);
						}
					}
				});
				if (map.size()>0) {
					AvailableMediaClips = map.values().toArray(new MediaClip[0]);
					return true;
				}
				
			}
			return false;
		}
		
		/**
		 * Play Media Clip
		 * @param AudioID Audio Output ID, start from 0
		 * @param ClipID start from 0, get from MediaClip Object
		 * @return true if can be played
		 */
		public boolean PlayClip(int AudioID, int ClipID) {
			String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=play","clip="+ClipID,"audiooutput="+(AudioID+1));
			if (common.ValidString(result)) {
				raise_log(result);
				if (result.contains("OKplaying=")) {
					return true;
				}
			} 
			return false;
		}
		
		/**
		 * Play Media Clip
		 * @param AudioID Audio Output ID, start from 1
		 * @param ClipID start from 0, from MediaClip Object
		 * @param Repeat -1 = forever, 0 = play once, N = repeat again N times
		 * @param Volume 0 - 1000, 0 = mute, default at 100
		 * @return String result
		 */
		public String PlayClip2(int AudioID, int ClipID, int Repeat, int Volume) {
			if (AudioID<1) AudioID=1;
			if (ClipID<0) ClipID=0;
			if (Repeat<-1) Repeat = 0;
			if (Volume<0) Volume = 0;
			if (Volume>1000) Volume = 1000;
			
			return HTTPGet_GetString("text/plain", url_playclip,"clip="+ClipID,"audiooutput="+AudioID, "repeat="+Repeat, "volume="+Volume);
			
		}
		
		/**
		 * Remove Media Clip
		 * @param ClipID start from 0, get from MediaClip object
		 * @return true if can be removed
		 */
		public boolean RemoveClip(int ClipID) {
			String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=remove","clip="+ClipID);
			if (common.ValidString(result)) {
				if (result.contains("removed=")) {
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Stop current playback
		 * @return true if can be stopped
		 */
		public boolean StopClip() {
			String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=stop");
			if (common.ValidString(result)) {
				if (result.contains("OKstopping")) {
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Download Media Clip 
		 * result is bytes of array, content of file format .au (G711 u-law)
		 * sampling rate 8 Khz, mono, 8 bit u-law
		 * @param ClipID start from 0
		 * @return array of bytes or null if failed
		 */
		public byte[] DownloadClip_ToByteArray(int ClipID) {
			if (ClipID<0) ClipID = 0;
			return HTTPGet_BytesResult("audio/basic",url_mediaclip,"action=download","clip="+ClipID );
		}
		
		/**
		 * Download Media Clip
		 * @param ClipID start from 0
		 * @param savelocation Save location
		 * @return DownloadResult object if success, or null if failed
		 */
		public DownloadResult DownloadClip_ToFile(int ClipID, String savelocation) {
			if (ClipID<0) ClipID = 0;
			
			DownloadResult dr = HTTPGet_DownloadFile(savelocation, url_mediaclip, "action=download","clip="+ClipID );
			return dr;
		}
		
		/**
		 * Upload a Media Clip
		 * Not implemented yet
		 * @param clipname Name to remember for this Clip
		 * @param FileName File path to upload 
		 * @return clip ID if success, or -1 if failed
		 */
		public int UploadClip(String clipname, String FileName) {
			return HTTPPost_UploadFile(FileName, url_mediaclip,"action=upload","media=audio","name="+clipname);
			
		}
		
		/**
		 * Update a Media Clip at ClipID slot
		 * Not implemented yet
		 * @param ClipID start from 0
		 * @param ClipName Clip Description
		 * @return true if success
		 */
		public boolean RenameClip(int ClipID, String ClipName) {
			String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=update","clip="+ClipID,"name="+ClipName);
			//System.out.println(result);
			if (common.ValidString(result)) {
				if (result.contains("updated=")) {
					return true;
				}
			}
			return false;
			
		}
		
		/**
		 * Check Supported AudioClip file format
		 * @return empty String array if failed
		 */
		public String[] AudioClipFormat() {
			String result = GetParameterValue("Properties.AudioClip.Format");
			if (common.ValidString(result)) {
				return result.split(",");
			} else return new String[0];
		}
		
		/**
		 * Dipakai untuk hasil GetAvailableMediaClips
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("MediaClip")
		public class MediaClip{
			public Integer ID;
			public String Name;
			public String Location;
			public String Type;
		}
	}
	
	
	/**
	 * Class to group Methods for Audio Relay Service API
	 * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10095155/display
	 * @author rdkartono
	 *
	 */
	public class AudioRelayServiceApi{
		private final String url = "/vapix/audiorelay";
		
		/**
		 * Check if Audio Relay Service is supported
		 * @return true if supported
		 */
		public boolean IsSupported() {
			String ver = GetParameterValue("Properties.API.AudioRelay.Version");
			//raise_log("Properties.API.AudioControl.Version = "+ver);
			if (common.ValidString(ver) && ver.contains("1.")) return true; else return false;
		}
		
		
		/**
		 * Create Broadcast Pattern
		 * @param senderip IP address of sender / leader
		 * @param receiverip array of IP address for receivers / followers
		 * @param use_multicast if true, broadcast using multicast. if false, using unicast
		 * @return array of Audio Peer Id
		 */
		public String[] CreateBroadcastPattern(String senderip, String[] receiverip, boolean use_multicast) {
			//TODO belum tested
			if (common.ValidString(senderip)) {
				if (common.ValidArrayOfString(receiverip)) {
					// buat configuration
					JsonObject val = new SetAudioPeerConfigurations(senderip, receiverip, use_multicast).getValue();
					raise_log(gs.toJson(val));
					JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:SetAudioPeerConfigurations", val));
					if (result!=null && result.has("AudioPeerId")) {
						try {
							String[] api = gs.fromJson(result.get("AudioPeerId").getAsJsonArray(), String[].class);
							raise_log(gs.toJson(api));
							return api;
						} catch(IllegalStateException | NullPointerException | JsonSyntaxException e) {
							raise_log("SetAudioPeerConfigurations result not contain AudioPeerId array");
						}
						
					}
				}
			}
			
			
			return null;
		}
		
		/**
		 * Add receiver / follower to existing broadcast
		 * @param ipaddress IP Address of follower
		 * @param  name follower name / description if necessary
		 * @return AudioPeerId string, or null if failed
		 */
		public String AddReceiver(String ipaddress, String name) {
			//TODO belum tested
			SetAudioPeerConfiguration sap = new SetAudioPeerConfiguration(ipaddress, name);
			JsonObject jo = new JsonObject();
			jo.add("axar:SetAudioNetworkConfiguration", sap.getValue());
			raise_log(gs.toJson(jo));
			
			JsonObject result = HTTPPost(url, jo);
			if (result!=null && result.has("AudioPeerId")) {
				return result.get("AudioPeerId").getAsString();
			}
			return null;
		}
		
		/**
		 * Remove Audio Peer
		 * @param AudioPeerId Audio Peer Id
		 * @return true if can be removed
		 */
		public boolean RemoveAudioPeer(String AudioPeerId) {
			//TODO belum tested
			if (common.ValidString(AudioPeerId)) {
				return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axar:RemoveAudioPeer", "AudioPeerId", AudioPeerId));		
			}
			return false;
		}
		
		/**
		 * Get Audio Peers
		 * @return array of AudioPeerConfiguration or null if failed
		 */
		public AudioPeer[] GetAudioPeers() {
			JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:GetAudioPeers", new JsonObject()));
			if (result!=null && result.has("Peer")) {
				try {
					JsonArray ja = result.get("Peer").getAsJsonArray();
					int length = ja.size();
					
					
					if (length>0) {
						AudioPeer[] apc = new AudioPeer[length];
						for(int ii=0;ii<length;ii++) {
							try {
								JsonElement je = ja.get(ii);
								//raise_log(MessageFormat.format("Peer [{0}] = {1}", ii, je.toString()));
								apc[ii] = gs.fromJson(je, AudioPeer.class);
							} catch(JsonSyntaxException | NullPointerException | IllegalStateException e) {
								raise_log("GetAudioPeers index="+ii+" Exception = "+e.getMessage());
							}
							
						}
						
						return apc;
					} 
				} catch(NullPointerException | IllegalStateException e) {
					raise_log("GetAudioPeers exception = "+e.getMessage());
				}
			}
			return null;
		}
		
		/**
		 * Get Audio Peer Status
		 * @return array of PeerStatus or null if failed
		 */
		public PeerStatus[] GetAudioPeerStatus() {
			JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:GetAudioPeerStatus", new JsonObject()));
			if (result!=null && result.has("PeerStatus")) {
				//raise_log(gs.toJson(result));
				try {
					JsonArray ps = result.get("PeerStatus").getAsJsonArray(); 
					PeerStatus[] peerstatus = gs.fromJson(ps, PeerStatus[].class);
					return peerstatus;
				} catch(IllegalStateException | NullPointerException | JsonSyntaxException e) {
					raise_log("GetAudioPeerStatus result not contain PeerStatus");
				}
			}
			return null;
		}
		
		/**
		 * Get Discovered Audio Peer
		 * @return array of DiscoveredAudioPeer or null if failed
		 */
		public DiscoveredAudioPeer[] GetDiscoveredAudioPeers() {
			JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:GetDiscoveredAudioPeers", new JsonObject()));
			if (result!=null && result.has("Peer")) {
				//raise_log(gs.toJson(result));
				try {
					JsonArray ja = result.get("Peer").getAsJsonArray();
					DiscoveredAudioPeer[] peer = gs.fromJson(ja, DiscoveredAudioPeer[].class);
					return peer;
				} catch(IllegalStateException | NullPointerException | JsonSyntaxException e) {
					raise_log("GetDiscoveredAudioPeers not contain Peer object");
				}
				
			}
			return null;
		}
		
		/**
		 * Set Sound Configuration
		 * @param Configuration Sound Configuration Object
		 * @return true if success
		 */
		public boolean SetSoundConfiguration(SoundConfiguration Configuration) {
			if (Configuration!=null) {
				return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axar:SetSoundConfiguration", "Configuration", Configuration.getValue()));
			}
			return false;
		}
		
		/**
		 * Set Sound Configuration, directly 
		 * @param Vol Master Volume
		 * @param mute if true then mute
		 * @return true if success
		 */
		public boolean SetSoundConfiguration_MasterVolume(int Vol, boolean mute) {
			SoundConfiguration conf = GetSoundConfiguration();
			if (conf!=null) {
				conf.MasterVolume = Vol;
				conf.MasterVolumeMute = mute;
				return SetSoundConfiguration(conf);
			}
			return false;
		}
		
		/**
		 * Get Sound Configuration
		 * @return null if failed
		 */
		public SoundConfiguration GetSoundConfiguration() {
			JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:GetSoundConfiguration", new JsonObject()));
			if (result!=null && result.has("Configuration")) {
				try {
					JsonElement je = result.get("Configuration");
					//raise_log(gs.toJson(je));
					return gs.fromJson(je, SoundConfiguration.class);
				} catch(IllegalStateException | NullPointerException | JsonSyntaxException e) {
					raise_log("GetSoundConfiguration exception = "+e.getMessage());
				}
				
			}
			return null;
		}
		
		@BA.ShortName("SoundConfiguration")
		@ToString
		public class SoundConfiguration{
			public int MasterVolume;
			public String MasterVolumeUnit;
			public boolean MasterVolumeMute;
			
			public JsonObject getValue() {
				JsonObject value = new JsonObject();
				value.addProperty("MasterVolume", MasterVolume);
				value.addProperty("MasterVolumeUnit", MasterVolumeUnit);
				value.addProperty("MasterVolumeMute", MasterVolumeMute);
				return value;
			}
		}
		
		/**
		 * Dipake untuk axar:SetAudioPeerConfiguration
		 * @author rdkartono
		 *
		 */
		public class SetAudioPeerConfiguration{
			private final JsonObject Configuration;
			/**
			 * Create new SetAudioPeerConfiguration
			 * @param ip IP Address
			 * @param name Name if necessary
			 */
			public SetAudioPeerConfiguration(String ip, String name) {
				Configuration = new JsonObject();
				if (common.ValidString(name)) Configuration.addProperty("Name", name);
				if (common.ValidString(ip)) {
					JsonObject Address = new JsonObject();
					Address.addProperty("IPAddress", ip);
					Configuration.add("Address", Address);
				}
			}
			
			public JsonObject getValue() {
				return Configuration;
			}
		}
		
		
		/**
		 * Dipake untuk axar:SetAudioPeerConfigurations Configuration
		 * @author rdkartono
		 *
		 */
		public class SetAudioPeerConfigurations{
			private final JsonArray Configuration;
			private final JsonObject AudioNetworkConfiguration;
			
			/**
			 * Create new  SetAudioPeerConfigurations
			 * @param leaderip Leader IP Address
			 * @param followers array of IP Address for followers
			 * @param multicast true for using multicast
			 */
			public SetAudioPeerConfigurations(String leaderip, String[] followers, boolean multicast) {
				AudioNetworkConfiguration = new JsonObject();
				AudioNetworkConfiguration.addProperty("MulticastEnabled", multicast);
				
				Configuration = new JsonArray();
				Configuration.add(createmember(1,leaderip, true));
				for(int ii=0;ii<followers.length;ii++) {
					Configuration.add(createmember(ii+2, followers[ii],false));
				}
			}
			
			/**
			 * Get Value structure for SetAudioPeerConfigurations
			 */
			public JsonObject getValue() {
				JsonObject cmd = new JsonObject();
				cmd.add("Configuration", Configuration);
				cmd.add("AudioNetworkConfiguration", AudioNetworkConfiguration);
				return cmd;
			}
			
			private JsonObject createmember(int id, String address, boolean leader) {
				JsonObject nn = new JsonObject();
				nn.addProperty("IPAddress", address);
				
				JsonObject mm = new JsonObject();
				mm.addProperty("Id", id);
				mm.addProperty("Leader", leader);
				mm.add("Address", nn);
				
				return mm;
			}
			
		}
		
		/**
		 * Dipakai untuk hasil axar:GetAudioPeers
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("AudioPeer")
		@ToString
		public class AudioPeer{
			public AudioPeerConfiguration Configuration;
			public AudioPeerMetaData MetaData;
			public String ConnectionStatus;
			public GainDefinition[] OutputGainDefinitions;
		}
		
		@ToString
		public class AudioPeerConfiguration{
			public AudioPeerAddress Address;
			public Gain[] OutputGain;
			public String Id;
			public String Name;
			public boolean Leader;
			public AudioPeerCredentials Credentials;
		}
		
		@ToString
		public class GainDefinition{
			public String Name;
			public String[] Range;
		}
		
		/**
		 * Dipake untuk hasil axar:GetAudioPeerStatus
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("PeerStatus")
		@ToString
		public class PeerStatus{
			public String Id;
			public String ConnectionStatus;
		}
		
		
		/**
		 * Dipake di GetDiscoveredAudioPeers
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("DiscoveredAudioPeer")
		@ToString
		public class DiscoveredAudioPeer{
			public AudioPeerAddress Address;
			public AudioPeerMetaData MetaData;
			public String Name;
			public boolean Leader;
		}
		
		@ToString
		public class AudioNetworkConfiguration{
			public String Name;
			public String Description;
			public boolean MulticastEnabled;
			public String MulticastGroup;
		}
		
		@ToString
		public class AudioPeerAddress{
			public String IPAddress;
			public String MAC;
		}
		
		@ToString
		public class Gain{
			public String Name;
			public String Value;
		}
		
		@ToString
		public class AudioPeerCredentials{
			public String User;
			public String Password;
		}
		
		
		@ToString
		public class AudioPeerMetaData{
			public String Type;
		}
		
		
	}
	
	
	/**
	 * Class to group Methods for Audio Control Service API
	 * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10088434/display
	 * @author rdkartono
	 *
	 */
	public class AudioControlServiceApi{
		public Volume Volume;
		public ControlCapabilities ControlCapabilities;
		
		private final String url = "/vapix/audiocontrol";
		public AudioControlServiceApi() {
			
		}
		
		
		public boolean IsSupported() {
			String ver = GetParameterValue("Properties.API.AudioControl.Version");
			//raise_log("Properties.API.AudioControl.Version = "+ver);
			if (common.ValidString(ver) && ver.contains("1.")) return true; else return false;
		}
		
		/**
		 * Get Volume Control ranges permitted by Device
		 * @return true if can be get
		 */
		public boolean GetControlCapabilities() {
			ControlCapabilities = null;		// bisa null
			JsonObject result = HTTPPost(url,common.Create_1Level_Command("axac:GetControlCapabilities", new JsonObject()));
			if (result!=null) {
				try {
					ControlCapabilities = gs.fromJson(result.get("Capabilities").getAsJsonObject(), ControlCapabilities.class) ;
					raise_log("ControlCapabilities = "+ControlCapabilities.toString());
					return true;
				} catch(NullPointerException | IllegalStateException | JsonSyntaxException e) {
					raise_log("axac:GetControlCapabilities not returning JsonObject Capabilities");
				}
			} else raise_log("GetControlCapabilities HTTPPost return null");
			return false;
		}
		
		/**
		 * Re-Get Volume from Device
		 * @return true if can be get
		 */
		public boolean GetVolume() {
			Volume = null;	// bisa null
			JsonObject result = HTTPPost(url,common.Create_1Level_Command("axac:GetVolume", new JsonObject()));
			if (result!=null) {
				try {
					Volume = gs.fromJson(result.get("Volume").getAsJsonObject(), Volume.class) ;
					raise_log("Volume = "+Volume.toString());
					return true;
				} catch(NullPointerException | IllegalStateException | JsonSyntaxException e) {
					raise_log("axac:GetVolume not returning JsonObject Volume");
				}
			} else raise_log("GetVolume HTTPPost return null");
			return false;
		}
		
		/**
		 * Set Volume to Device
		 * @return true if can be set
		 */
		public boolean SetVolume() {
			//TODO belum ditest
			if (Volume!=null) {
				
				JsonObject result = HTTPPost(url,common.Create_2Level_Command("axac:SetVolume", "Volume", Volume.getValue()));
				if (result!=null) {
					return true;
				}
			}
			
			return false;
		}
		
		/**
		 * Dipakai untuk hasil axac:GetControlCapabilities
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("ControlCapabilities")
		@ToString
		public class ControlCapabilities{
			public VolumeRange[] VolumeRanges;
			public String[] InputIds;
			public String[] ChannelIds;
		}
		
		@BA.ShortName("VolumeRange")
		@ToString
		public class VolumeRange{
			public String Unit;
			public int MaxValue;
			public int MinValue;
			
		}
		
		/**
		 * Dipakai untuk hasil axac:GetVolume dan axac:SetVolume
		 * @author rdkartono
		 *
		 */
		@BA.ShortName("Volume")
		@ToString
		public class Volume{
			public int ForegroundVolume;
			public String ForegroundVolumeUnit;
			public boolean ForegroundVolumeMute;
			public int BackgroundVolume;
			public String BackgroundVolumeUnit;
			public boolean BackgroundVolumeMute;
			
			public JsonObject getValue() {
				JsonObject value = new JsonObject();
				value.addProperty("ForegroundVolume", ForegroundVolume);
				value.addProperty("ForegroundVolumeUnit", ForegroundVolumeUnit);
				value.addProperty("ForegroundVolumeMute", ForegroundVolumeMute);
				value.addProperty("BackgroundVolume", BackgroundVolume);
				value.addProperty("BackgroundVolumeUnit", BackgroundVolumeUnit);
				value.addProperty("BackgroundVolumeMute", BackgroundVolumeMute);
				return value;
			}
		}
		
		
	}
	
	/**
	 * Class to group Methods for Call Service API
	 * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10046341/display
	 * @author rdkartono
	 *
	 */
	public class CallServiceApi{
		private final String url = "/vapix/call";
		public SIPConfiguration SIPConfiguration;
		public SIPAccounts SIPAccounts;
		
		public CallServiceApi() {
			SIPConfiguration = new SIPConfiguration();
			SIPAccounts = new SIPAccounts();
		}
		
		/**
		 * Check if Support SIP Service
		 * @return true if supported
		 */
		public boolean IsSupported() {
			String sip = GetParameterValue("Properties.API.SIP.SIP");
			String sipver = GetParameterValue("Properties.API.SIP.Version");
			if (common.ValidString(sip)) {
				if (common.ValidString(sipver)) {
					return sip.equals("yes");
				}
			}
			return false;
		}
				
		
		
		
		
		public class SIPAccounts{
			private JsonArray Accounts;
			public SIPAccounts() {
				Accounts = new JsonArray();
			}
			
			/**
			 * Re-Get SIP Accounts registered at Device
			 * @return true if can be get
			 */
			public boolean Get() {
				
				JsonObject result = HTTPPost(url,common.Create_1Level_Command("axcall:GetSIPAccounts", new JsonObject()));
				if (result!=null) {
					try{
						Accounts = result.get("SIPAccount").getAsJsonArray();
						//raise_log("SIP Accounts = "+Accounts.toString());
						return true;
					} catch(IllegalStateException | NullPointerException e) {
						raise_log("axcall:GetSIPAccounts not returning SIPAccount array");
					}
				}
				return false;
			}
			
			/**
			 * Get Registered SIP Accounts
			 * @return array of SIPAccount object
			 */
			public SIPAccount[] Registered_SIP_Accounts() {
				
				if (Accounts!=null) {
					return Accounts.asList().stream().map(jo -> new SIPAccount(jo.getAsJsonObject())).toArray(SIPAccount[]::new);
					
				}
				return new SIPAccount[0];
			}
			
			/**
			 * Get SIP Account Status 
			 * @param SIPAccountId Account ID
			 * @return null if failed
			 */
			public SIPAccountStatus GetSIPAccountStatus(String SIPAccountId) {
				if (common.ValidString(SIPAccountId)) {
					JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:GetSIPAccountStatus", "SIPAccountId", SIPAccountId));
					if (result!=null && result.has("SIPAccountStatus")) {
						//raise_log(result.toString());
						return new SIPAccountStatus(common.GetJsonObjectValue_JsonObject(result, "SIPAccountStatus"));
					}
				}
				return null;
			}
			
			/**
			 * Add a SIP User
			 * @param UserId Extension Number or user ID
			 * @param Password  Password
			 * @param Registrar SIP Server
			 * @param PublicDomain Domain where this account 
			 * @return String SIPAccountId or null if failed
			 */
			public String SetSIPAccount(String UserId, String Password, String Registrar, String PublicDomain) {
				//TODO belum ditest
				JsonObject jc = new JsonObject();
				if (common.ValidString(UserId)) jc.addProperty("UserId", UserId);
				if (common.ValidString(Password)) jc.addProperty("Password", Password);
				if (common.ValidString(Registrar)) jc.addProperty("Registrar", Registrar);
				if (common.ValidString(PublicDomain)) jc.addProperty("PublicDomain", PublicDomain);
				
				JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPAccount", "SIPAccount", jc));
				if (result!=null && result.has("SIPAccountId")) return common.GetJsonObjectValue_String(result, "SIPAccountId");
				return null;
			}
			
			/**
			 * Add a SIP user
			 * @param UserId extension number or user iD
			 * @param Password password
			 * @param Registrar SIP Server
			 * @param PublicDomain Domain where this SIP Account
			 * @param proxies Proxy Server if required
			 * @return SIP Account ID
			 */
			public String SetSIPAccount(String UserId, String Password, String Registrar, String PublicDomain, SIPProxyServer[] proxies) {
				//TODO belum di test
				JsonObject jc = new JsonObject();
				if (common.ValidString(UserId)) jc.addProperty("UserId", UserId);
				if (common.ValidString(Password)) jc.addProperty("Password", Password);
				if (common.ValidString(Registrar)) jc.addProperty("Registrar", Registrar);
				if (common.ValidString(PublicDomain)) jc.addProperty("PublicDomain", PublicDomain);
				if (proxies!=null && proxies.length>0) {
					JsonArray jd = new JsonArray();
					for(SIPProxyServer prox : proxies) jd.add(prox.getJsonObject());
					jc.add("SIPAccount", jd);
				}
				JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPAccount", "SIPAccount", jc));
				if (result!=null && result.has("SIPAccountId")) return common.GetJsonObjectValue_String(result, "SIPAccountId");
				return null;
			}
			
			/**
			 * Try to call another device
			 * Use SIP URI in format sip:extension@publicdomain
			 * example sip:6002@example.axis.com
			 * @param SIPURI SIP URI. Get it from Registered_SIP_Accounts
			 * @return CallID if success or null if failed
			 */
			public String Call(String SIPURI) {
				if (common.ValidString(SIPURI)) {
					JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:Call", "To", SIPURI));
					if (result!=null && result.has("CallId")) {
						return common.GetJsonObjectValue_String(result, "CallId");
					}
				} return null;
			}
			
			/**
			 * Get Call Status
			 * @param CallId CallId , result of Call function
			 * @return SIPCallStatus object, or null if failed
			 */
			public SIPCallStatus GetCallStatus(String CallId) {
				if (common.ValidString(CallId)) {
					JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetCallStatus", "CallId", CallId));
					if (result!=null && result.has("CallStatus")) {
						SIPCallStatus scs =  gs.fromJson(result.get("CallStatus"), SIPCallStatus.class);
						System.out.println(scs.toString());
						return scs;
					}
				}
				return null;
			}
			
			/**
			 * Get All Call Statuses
			 * @return SIPCallStatus array object, or null if failed
			 */
			public SIPCallStatus[] GetCallStatuses() {
				JsonObject result = HTTPPost(url, common.Create_1Level_Command("axcall:GetCallStatuses", new JsonObject()));
				if (result!=null && result.has("CallStatus")) {
					try {
						JsonArray ja = result.get("CallStatus").getAsJsonArray();
						SIPCallStatus[] vv = ja.asList()
								.stream()
								.map(je -> gs.fromJson(je, SIPCallStatus.class))
								.toArray(SIPCallStatus[]::new);
						if (vv!=null && vv.length>0) {
							for(SIPCallStatus vx : vv) {
								System.out.println(vx);
							}
							
							return vv;
						}
					} catch(IllegalStateException e) {
						raise_log("GetCallStatuses exception="+e.getMessage());
					}
					
				}
				return null;
			}
			
			/**
			 * Terminate a call
			 * @param CallId CallId, result of Call function
			 * @return true if success
			 */
			public boolean TerminateCall(String CallId) {
				if (common.ValidString(CallId)) {
					return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:TerminateCall", "CallId", CallId));
				}
				return false;
			}
			
			/**
			 * Create a DTMF Event
			 * @param Name Name of event 
			 * @param Enabled true or false
			 * @return DTMFEventID string if success, or null if failed
			 */
			public String SetDTMFEvent(String Name, boolean Enabled) {
				//TODO belum ditest
				if (common.ValidString(Name)) {
					JsonObject ja = new JsonObject();
					ja.addProperty("Name", Name);
					ja.addProperty("Enabled", Enabled);
					
					JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:SetDTMFEvent", "DTMFEvent", ja));
					if (result!=null && result.has("DTMFEventId")) {
						return common.GetJsonObjectValue_String(result, "DTMFEventId");
					}
				}
				return null;
			}
			
			/**
			 * Remove a DTMF Event
			 * @param DTMFEventId value from SetDTMFEvent
			 * @return true if can be removed
			 */
			public boolean RemoveDTMFEvent(String DTMFEventId) {
				//TODO belum ditest
				if (common.ValidString(DTMFEventId)) {
					return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:RemoveDTMFEvent", "DTMFEventId", DTMFEventId));
				}
				return false;
			}
			
			/**
			 * Create DTMF Trigger 
			 * @param DTMFSequence DTMF Code that trigger a DTMF Event
			 * @param DTMFEventId DTMF Event Id from SetDTMFEvent
			 * @return DTMFTriggerId if success, or null if failed
			 */
			public String SetDTMFTrigger(String DTMFSequence, String DTMFEventId) {
				//TODO belum ditest
				if (common.ValidString(DTMFSequence)) {
					if (common.ValidString(DTMFEventId)) {
						JsonObject ja = new JsonObject();
						ja.addProperty("DTMFSequence", DTMFSequence);
						ja.addProperty("DTMFEventId", DTMFEventId);
						JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall.SetDTMFTrigger", "DTMFTrigger", ja));
						if (result!=null && result.has("DTMFTriggerId")) {
							return common.GetJsonObjectValue_String(result, "DTMFTriggerId");
						}
						
					}
				}
				return null;	
			}
			
			/**
			 * Get DTMFTrigger configuration from DTMFTriggerId
			 * @param DTMFTriggerId DTMF Trigger Id
			 * @return null if failed
			 */
			public DTMFTrigger GetDTMFTrigger(String DTMFTriggerId) {
				//TODO belum ditest
				if (common.ValidString(DTMFTriggerId)) {
					JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetDTMFTrigger", "DTMFTriggerId", DTMFTriggerId));
					if (result!=null && result.has("DTMFTrigger")) {
						return new DTMFTrigger(common.GetJsonObjectValue_JsonObject(result, "DTMFTrigger"));
					}
				}
				return null;
			}
			
			/**
			 * Create DTMF Configuration
			 * @param Name some name
			 * @param DTMFTriggerId DTMF Trigger Id
			 * @return DTMFConfigurationId if success, or null if failed
			 */
			public String SetDTMFConfiguration(String Name, String DTMFTriggerId) {
				//TODO belum ditest
				if (common.ValidString(Name)) {
					if (common.ValidString(DTMFTriggerId)) {
						JsonObject jo = new JsonObject();
						jo.addProperty("Name", Name);
						jo.addProperty("DTMFTriggerId", DTMFTriggerId);
						
						JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:SetDTMFConfiguration", "DTMFConfiguration", jo));
						if (result!=null && result.has("DTMFConfigurationId")) {
							return common.GetJsonObjectValue_String(result, "DTMFConfigurationId");
						}
					}
				}
				return null;
			}
			
			/**
			 * Remove DTMF Configuration
			 * @param DTMFConfigurationId DTMF Configuration Id
			 * @return true if can be removed
			 */
			public boolean RemoveDTMFConfiguration(String DTMFConfigurationId) {
				//TODO belum ditest
				if (common.ValidString(DTMFConfigurationId)) {
					return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:RemoveDTMFConfiguration", "DTMFConfigurationId", DTMFConfigurationId));
				}
				return false;
			}
			
			/**
			 * Get DTMF Configuration
			 * @param DTMFConfigurationId DTMF Configuration Id
			 * @return DTMFConfiguration if success, or null if failed
			 */
			public DTMFConfiguration GetDTMFConfiguration(String DTMFConfigurationId) {
				//TODO belum ditest
				if (common.ValidString(DTMFConfigurationId)) {
					JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetDTMFConfiguration", "DTMFConfigurationId", DTMFConfigurationId));
					if (result!=null && result.has("DTMFConfiguration")) {
						return new DTMFConfiguration(common.GetJsonObjectValue_JsonObject(result, "DTMFConfiguration"));
					}
				}
				return null;
			}
			
			@BA.ShortName("DTMFConfiguration")
			public class DTMFConfiguration{
				public String Name;
				public String DTMFTriggerId;
				public String Id;
				public boolean RFC2833;
				public boolean RFC2976;
				public DTMFConfiguration(JsonObject jo) {
					if (jo!=null) {
						Name = common.GetJsonObjectValue_String(jo, "Name");
						DTMFTriggerId = common.GetJsonObjectValue_String(jo, "DTMFTriggerId");
						Id = common.GetJsonObjectValue_String(jo, "Id");
						RFC2833 = common.GetJsonObjectValue_Boolean(jo, "RFC2833");
						RFC2976 = common.GetJsonObjectValue_Boolean(jo, "RFC2976");
					}
				}
			}
			
			@BA.ShortName("DTMFTrigger")
			public class DTMFTrigger{
				public String Id;
				public String DTMFSequence;
				public String DTMFEventId;
				public DTMFTrigger(JsonObject jo) {
					if (jo!=null) {
						Id = common.GetJsonObjectValue_String(jo, "Id");
						DTMFSequence = common.GetJsonObjectValue_String(jo, "DTMFSequence");
						DTMFEventId = common.GetJsonObjectValue_String(jo, "DTMFEventId");
					}
				}
			}
			
			@BA.ShortName("SIPCallStatus")
			@ToString
			public class SIPCallStatus{
				
				/**
				 * Call Identifier
				 */
				public String CallId;
				
				/**
				 * Source dari call.. tapi belum konfirm bener
				 */
				public String Source;
				
				/**
				 * Active / Terminated
				 */
				public String CallState;
				
				
				public String CallStateReason;
				
				/**
				 * Kapan incoming call terjadi
				 */
				public String StartTime;
				
				/**
				 * Kapan incoming call selesai
				 * Kalau belum selesai, null
				 */
				public String StopTime;
				
				/**
				 * Pada receiver, Direction = Incoming
				 */				public String Direction;
				
				/**
				 * SIP Account diri sendiri <nomor_extension@server_sip>
				 */
				public String DeviceURI;
				
				/**
				 * SIP Account yang nge-call device ini <nomor_extension@server_sip>
				 */
				public String RemoteURI;
				
				/**
				 * Codec = PCMU/8000
				 */
				public String AudioCodec;
				
				/**
				 * SIP Account ID diri sendiri
				 */
				public String SIPAccountId;
				
				/**
				 * value = SIP
				 */
				public String CallType;
				
				/**
				 * Kalau gak ada, null
				 */
				public String VideoCodec;
				
				/**
				 * CallState = Active atau Terminated ?
				 * @return true kalau masih calling
				 */
				public boolean CallingIsActive() {
					return "Active".equals(CallState);
				}
				
				
			}
			
			@BA.ShortName("SIPProxyServer")
			public class SIPProxyServer{
				private JsonObject jo;
				public SIPProxyServer() {
					jo = new JsonObject();
				}
				
				public SIPProxyServer(JsonObject jo) {
					if (jo!=null) {
						this.jo = jo;
					}
				}
				
				public String getServer() {
					return common.GetJsonObjectValue_String(jo, "Server");
				}
				
				public void setServer(String value) {
					common.SetJsonObjectValue(jo, "Server", value);
				}
				
				public String getUsername() {
					return common.GetJsonObjectValue_String(jo, "Username");
				}
				
				public void setUsername(String value) {
					common.SetJsonObjectValue(jo, "Username", value);
				}
				
				public String getPassword() {
					return common.GetJsonObjectValue_String(jo, "Password");
				}
				
				public void setPassword(String value) {
					common.SetJsonObjectValue(jo, "Password", value);
				}
				
				public JsonObject getJsonObject() {
					return jo;
				}
			}
			
			@BA.ShortName("SIPAccountStatus")
			public class SIPAccountStatus{
				public String SIPAccountId;
				public String SIPURI;
				public boolean IsDefault;
				public String RegURI;
				public int Status;
				public String StatusText;
				
				public SIPAccountStatus(JsonObject jo) {
					if (jo!=null) {
						this.SIPAccountId = common.GetJsonObjectValue_String(jo, "SIPAccountId");
						this.SIPURI = common.GetJsonObjectValue_String(jo, "SIPURI");
						this.IsDefault = common.GetJsonObjectValue_Boolean(jo, "IsDefault");
						this.RegURI = common.GetJsonObjectValue_String(jo, "RegURI");
						this.Status = common.GetJsonObjectValue_Integer(jo, "Status");
						this.StatusText = common.GetJsonObjectValue_String(jo, "StatusText");
					}
				}
				
				@Override
				public String toString() {
					return MessageFormat.format("Id:{0}, SIPURI:{1}, REGURI:{2}, Status:{3}, StatusText:{4}", SIPAccountId, SIPURI, RegURI, Status, StatusText);
				}
			}
			
			@BA.ShortName("SIPAccount")
			public class SIPAccount{
				public String Id;
				public Boolean Enabled;
				public String Name;
				public String CallerId;
				public String UserId;
				public String Password;
				public String Registrar;
				public String PublicDomain;
				public Boolean IsDefault;
				public Boolean AutoAnswerEnabled;
				public String Transport;
				public String StreamParameters;
				public String DTMFConfigurationId;
				public String MediaEncryption;
				
				public SIPAccount(JsonObject jo) {
					if (jo!=null) {
						Id = common.GetJsonObjectValue_String(jo, "Id");
						Enabled = common.GetJsonObjectValue_Boolean(jo, "Enabled");
						Name = common.GetJsonObjectValue_String(jo, "Name");
						CallerId = common.GetJsonObjectValue_String(jo, "CallerId");
						UserId = common.GetJsonObjectValue_String(jo, "UserId");
						Password = common.GetJsonObjectValue_String(jo, "Password");
						Registrar = common.GetJsonObjectValue_String(jo, "Registrar");
						PublicDomain = common.GetJsonObjectValue_String(jo, "PublicDomain");
						IsDefault = common.GetJsonObjectValue_Boolean(jo, "IsDefault");
						AutoAnswerEnabled = common.GetJsonObjectValue_Boolean(jo, "AutoAnswerEnabled");
						Transport = common.GetJsonObjectValue_String(jo, "Transport");
						StreamParameters = common.GetJsonObjectValue_String(jo, "StreamParameters");
						DTMFConfigurationId = common.GetJsonObjectValue_String(jo, "DTMFConfigurationId");
						MediaEncryption = common.GetJsonObjectValue_String(jo, "MediaEncryption");
					}
				}
				
				@Override
				public String toString() {
					return MessageFormat.format("Id:{0}, UserId:{1}, Password:{2}, PublicDomain:{3}, Enabled:{4}", Id, UserId, Password, PublicDomain, Enabled);
				}
				
			}
		}
		
		public class SIPConfiguration{
			private JsonObject SIPConfiguration;
			public SIPConfiguration(){
				SIPConfiguration = new JsonObject();
			}
			
			/**
			 * Re-Get SIP Configuration from Device
			 * @return true if can be get
			 */
			public boolean Get() {
				JsonObject jo = new JsonObject();
				
				jo.add("axcall:GetSIPConfiguration", new JsonObject());
				//raise_log("jo = "+jo.toString());
				JsonObject result = HTTPPost(url,jo);
				if (result!=null) {
					try {
						SIPConfiguration = result.get("SIPConfiguration").getAsJsonObject();
						//raise_log("SIP Configuration = "+SIPConfiguration.toString());
						return true;
					} catch(IllegalStateException | NullPointerException e) {
						raise_log("axcall:GetSIPConfiguration not returning JsonObject");
					}
				}
				return false;
			}
			
			
			
			
			/**
			 * Set SIP Configuration to Device
			 * @return true if can be set
			 */
			public boolean Set() {
				JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPConfiguration", "SIPConfiguration", SIPConfiguration));
				raise_log("SetSIPConfiguration result = "+result.toString());
				return true;
			}
			
			public boolean getSIPEnabled() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "SIPEnabled");
			}
			
			public void setSIPEnabled(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "SIPEnabled", value);
			}
			
			public boolean getICEEnabled() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ICEEnabled");
			}
			
			public String[] getTURNServers() {
				return common.GetJsonObjectValue_Strings(SIPConfiguration, "TURNServers");
			}
			
			public void setTURNServers(String[] server) {
				common.SetJsonObjectValue(SIPConfiguration,"TURNServers" , server);
			}
			
			public String[] getSTUNServers() {
				return common.GetJsonObjectValue_Strings(SIPConfiguration, "STUNServers");
			}
			
			public void setSTUNServers(String[] server) {
				common.SetJsonObjectValue(SIPConfiguration,"STUNServers" , server);
			}
			
			public String[] getAllowedUsers() {
				return common.GetJsonObjectValue_Strings(SIPConfiguration, "AllowedUsers");
			}
			
			public void setAllowedUsers(String[] users) {
				common.SetJsonObjectValue(SIPConfiguration,"AllowedUsers" , users);
			}
			
			public String[] getAllowedURIs() {
				return common.GetJsonObjectValue_Strings(SIPConfiguration, "AllowedURIs");
			}
			
			public void setAllowedURIs(String[] uri) {
				common.SetJsonObjectValue(SIPConfiguration,"AllowedURIs" , uri);
			}
			
			public void setICEEnabled(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "ICEEnabled", value);
			}
			
			public boolean getAllowIncomingCalls() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "AllowIncomingCalls");
			}
			
			public void setAllowIncomingCalls(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "AllowIncomingCalls", value);
			}
			
			public boolean getTURNEnabled() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "TURNEnabled");
			}
			
			public void setTURNEnabled(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "TURNEnabled", value);
			}
			
			public boolean getSTUNEnabled() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "STUNEnabled");
			}
			
			public void setSTUNEnabled(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "STUNEnabled", value);
			}
			
			public boolean getApplyUserAuthentication() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ApplyUserAuthentication");
			}
			
			public void setApplyUserAuthentication(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "ApplyUserAuthentication", value);
			}
			
			public boolean getApplyAllowedURIs() {
				return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ApplyAllowedURIs");
			}
			
			public void setApplyAllowedURIs(boolean value) {
				common.SetJsonObjectValue(SIPConfiguration, "ApplyAllowedURIs", value);
			}
			
			public int getSIPPort() {
				return common.GetJsonObjectValue_Integer(SIPConfiguration, "SIPPort");
			}
			
			public void setSIPPort(int value) {
				common.SetJsonObjectValue(SIPConfiguration, "SIPPort", value);
			}
			
			public int getSIPTLSPort() {
				return common.GetJsonObjectValue_Integer(SIPConfiguration, "SIPTLSPort");
			}
			
			public void setSIPTLSPort(int value) {
				 common.SetJsonObjectValue(SIPConfiguration, "SIPTLSPort", value);
			}
			
		}
		
		
		
	}
}
