Can successfully start an activity on power on.
This commit is contained in:
parent
ea5b7ba054
commit
a7ae259567
@ -2,15 +2,16 @@ import { Activity } from '../Models/Activity';
|
|||||||
import { Matrix } from '../Models/Matrix';
|
import { Matrix } from '../Models/Matrix';
|
||||||
import { IAccessory } from './IAccessory';
|
import { IAccessory } from './IAccessory';
|
||||||
import callbackify from '../Util/Callbackify';
|
import callbackify from '../Util/Callbackify';
|
||||||
|
import HarmonyDataProvider from '../DataProviders/HarmonyDataProvider';
|
||||||
|
|
||||||
let Service: HAPNodeJS.Service;
|
let Service: HAPNodeJS.Service;
|
||||||
let Characteristic: HAPNodeJS.Characteristic;
|
let Characteristic: HAPNodeJS.Characteristic;
|
||||||
let Api: any;
|
let Api: any;
|
||||||
|
|
||||||
export interface IControlUnitProps {
|
export interface IControlUnitProps {
|
||||||
|
dataProvider: HarmonyDataProvider,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
activities: Array<Activity>,
|
activities: Array<Activity>,
|
||||||
matrix: Matrix,
|
|
||||||
api: any,
|
api: any,
|
||||||
log: any,
|
log: any,
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ export class ControlUnit implements IAccessory {
|
|||||||
|
|
||||||
//Harmony fields
|
//Harmony fields
|
||||||
private activities: Array<Activity> = [];
|
private activities: Array<Activity> = [];
|
||||||
private matrix: Matrix;
|
private dataProvider: HarmonyDataProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -50,7 +51,8 @@ export class ControlUnit implements IAccessory {
|
|||||||
this.name = this.displayName;
|
this.name = this.displayName;
|
||||||
|
|
||||||
this.activities = props.activities;
|
this.activities = props.activities;
|
||||||
this.matrix = props.matrix;
|
|
||||||
|
this.dataProvider = props.dataProvider;
|
||||||
|
|
||||||
//Configure services
|
//Configure services
|
||||||
this.configureTvService();
|
this.configureTvService();
|
||||||
@ -100,12 +102,19 @@ export class ControlUnit implements IAccessory {
|
|||||||
//TODO
|
//TODO
|
||||||
private onSetAccessoryActive = async (value: any) => {
|
private onSetAccessoryActive = async (value: any) => {
|
||||||
this.log(`set active + ${value}`);
|
this.log(`set active + ${value}`);
|
||||||
|
switch (value) {
|
||||||
|
case 0: this.dataProvider.powerOff(this.name); break;
|
||||||
|
case 1: this.dataProvider.powerOn(this.name, this.activities[0]); break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
private onGetAccessoryActive = async () => {
|
private onGetAccessoryActive = async () => {
|
||||||
this.log(`get active`)
|
this.log(`get active`)
|
||||||
return Characteristic.Active.Active;
|
|
||||||
|
//@ts-ignore
|
||||||
|
return this.dataProvider.getIsActive ? Characteristic.Active.Active : Characteristic.Active.Inactive
|
||||||
|
// return Characteristic.Active.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
@ -1,5 +1,275 @@
|
|||||||
|
import { Activity } from "../Models/Activity";
|
||||||
|
import { DeviceSetupItem } from "../Models/DeviceSetupItem";
|
||||||
|
import { threadId } from "worker_threads";
|
||||||
|
import { Input, Matrix, Output } from "../Models/Matrix";
|
||||||
|
|
||||||
|
const Harmony = require("harmony-websocket");
|
||||||
|
interface IDevice {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
supportsCommand(commandName: string): boolean,
|
||||||
|
getCommand(commandName: string): string,
|
||||||
|
commands: { [name: string]: string };
|
||||||
|
on: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IActivityState {
|
||||||
|
currentActivity: Activity
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHarmonyDataProviderProps {
|
||||||
|
hubAddress: string,
|
||||||
|
log: any,
|
||||||
|
matrix: Matrix
|
||||||
|
}
|
||||||
|
|
||||||
class HarmonyDataProvider {
|
class HarmonyDataProvider {
|
||||||
|
private harmony: any;
|
||||||
|
private log: any;
|
||||||
|
private hubAddress: string = "";
|
||||||
|
private connected: boolean = false;
|
||||||
|
|
||||||
|
private devices: { [name: string]: IDevice; } = {};
|
||||||
|
private states: { [controlUnitName: string]: (IActivityState | undefined) } = {};
|
||||||
|
|
||||||
|
private matrix: Matrix;
|
||||||
|
|
||||||
|
constructor(props: IHarmonyDataProviderProps) {
|
||||||
|
this.log = props.log;
|
||||||
|
this.hubAddress = props.hubAddress;
|
||||||
|
this.matrix = props.matrix;
|
||||||
|
|
||||||
|
this.harmony = new Harmony();
|
||||||
|
|
||||||
|
//Listeners
|
||||||
|
this.harmony.on('open', () => {
|
||||||
|
this.log('Hub open');
|
||||||
|
this.connected = true;
|
||||||
|
});
|
||||||
|
this.harmony.on('close', () => {
|
||||||
|
this.log('Hub closed');
|
||||||
|
this.connected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public powerOn = async (controlUnitName: string, activity: Activity) => {
|
||||||
|
await this.startActivity(controlUnitName, activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public powerOff = async (controlUnitName: string) => {
|
||||||
|
if (!this.states[controlUnitName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Build potential list of devices to turn off
|
||||||
|
let devicesToTurnOff: Array<IDevice> = this.states[controlUnitName]!.currentActivity.deviceSetupItems
|
||||||
|
.map((value: DeviceSetupItem): IDevice => {
|
||||||
|
return this.getDeviceFromName(value.deviceName);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Resolve device conflicts with other controlUnits
|
||||||
|
devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName);
|
||||||
|
|
||||||
|
//Turn off devices
|
||||||
|
devicesToTurnOff.forEach((device: IDevice) => {
|
||||||
|
this.powerOffDevice(device);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.states[controlUnitName] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public startActivity = async (controlUnitName: string, activity: Activity) => {
|
||||||
|
let lastActivity: Activity | undefined = undefined;
|
||||||
|
if (this.states[controlUnitName]) {
|
||||||
|
lastActivity = this.states[controlUnitName]!.currentActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.log(`asdf: ${JSON.stringify(this.devices)}`);
|
||||||
|
//Build potential list of devices to to turn on
|
||||||
|
let devicesToTurnOn: Array<IDevice> = activity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => {
|
||||||
|
return this.getDeviceFromName(value.deviceName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// this.log(`Devices to turn on: ${JSON.stringify(devicesToTurnOn)}`);
|
||||||
|
|
||||||
|
//Resolve device conflicts with other controlUnits
|
||||||
|
devicesToTurnOn = this.sanitizeDeviceList(devicesToTurnOn, controlUnitName);
|
||||||
|
// this.log(`Sanatized devices to turn on: ${JSON.stringify(devicesToTurnOn)}`)
|
||||||
|
|
||||||
|
//Turn on devices
|
||||||
|
await Promise.all(devicesToTurnOn.map(async (device: IDevice) => {
|
||||||
|
this.log(`Turning on device ${device.name}`)
|
||||||
|
await this.powerOnDevice(device);
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Assign correct input
|
||||||
|
await Promise.all(
|
||||||
|
activity.deviceSetupItems.map(async (value: DeviceSetupItem) => {
|
||||||
|
let device: IDevice = this.getDeviceFromName(value.deviceName);
|
||||||
|
|
||||||
|
if (device.supportsCommand(`Input ${value.input}`)) {
|
||||||
|
let command: string = device.getCommand(`Input ${value.input}`);
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (activity.useMatrix) {
|
||||||
|
//get input and output
|
||||||
|
let input: Input = this.matrix.inputs.filter(e => e.inputDevice === activity.controlDeviceId)[0];
|
||||||
|
let output: Output = this.matrix.outputs.filter(e => e.outputDevice === activity.outputDeviceId)[0];
|
||||||
|
|
||||||
|
let inputCommandName: string = `In ${input.inputNumber}`;
|
||||||
|
let outputCommandName: string = `Out ${output.outputLetter}`;
|
||||||
|
|
||||||
|
// this.log(`Matrix input command: ${inputCommandName} Matrix output command: ${outputCommandName}`);
|
||||||
|
|
||||||
|
let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName);
|
||||||
|
|
||||||
|
//Rout hdmi
|
||||||
|
if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) {
|
||||||
|
await this.sendCommand(matrixDevice.getCommand(inputCommandName));
|
||||||
|
await this.sendCommand(matrixDevice.getCommand(outputCommandName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build potential list of devices to turn off
|
||||||
|
if (lastActivity) {
|
||||||
|
let devicesToTurnOff: Array<IDevice> = lastActivity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => {
|
||||||
|
return this.getDeviceFromName(value.deviceName);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log(`Devices to turn off: ${JSON.stringify(devicesToTurnOff)}`);
|
||||||
|
|
||||||
|
//Resolve device conflicts with other controlUnits
|
||||||
|
devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName);
|
||||||
|
|
||||||
|
this.log(`Sanatized devices to turn off: ${JSON.stringify(devicesToTurnOff)}`);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
//Turn off devices
|
||||||
|
devicesToTurnOff.map(async (device: IDevice) => {
|
||||||
|
await this.powerOffDevice(device);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new HarmonyDataProvider();
|
//Assign current activity
|
||||||
|
this.states[controlUnitName] = { currentActivity: activity };
|
||||||
|
}
|
||||||
|
|
||||||
|
public volumeUp = async (controlUnitName: string) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public volumeDown = async (controlUnitName: string) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendKeyPress = async (controlUnitName: string) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIsActive(controlUnitName: string): boolean {
|
||||||
|
return this.states[controlUnitName] ? true : false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to harmony and receive device info
|
||||||
|
*/
|
||||||
|
private connect = async () => {
|
||||||
|
await this.harmony.connect(this.hubAddress);
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
setTimeout(async function () {
|
||||||
|
if (self.connected) {
|
||||||
|
let devices: any = await self.harmony.getDevices();
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
//Add each to dictionary
|
||||||
|
devices.map(async (dev: any) => {
|
||||||
|
//get commands
|
||||||
|
let commands: { [name: string]: string } = {};
|
||||||
|
let deviceCommands: any = await self.harmony.getDeviceCommands(dev.id);
|
||||||
|
deviceCommands.forEach((command: any) => {
|
||||||
|
commands[command.label] = command.action;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.devices[dev.label] = {
|
||||||
|
id: dev.id,
|
||||||
|
name: dev.label,
|
||||||
|
commands: commands,
|
||||||
|
on: false,
|
||||||
|
//Define device methods
|
||||||
|
supportsCommand(commandName: string): boolean {
|
||||||
|
let command = commands[commandName];
|
||||||
|
return (command) ? true : false;
|
||||||
|
},
|
||||||
|
getCommand(commandName: string): string {
|
||||||
|
return commands[commandName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
self.log(`ERROR - error connecting to harmony: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private powerOffDevice = async (device: IDevice) => {
|
||||||
|
let powerOffCommand: string = "Power Off";
|
||||||
|
if (device.supportsCommand(powerOffCommand)) {
|
||||||
|
await this.sendCommand(device.getCommand(powerOffCommand));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private powerOnDevice = async (device: IDevice) => {
|
||||||
|
let powerOnCommand: string = "Power On";
|
||||||
|
if (device.supportsCommand(powerOnCommand)) {
|
||||||
|
await this.sendCommand(device.getCommand(powerOnCommand));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDeviceFromName(deviceName: string): IDevice {
|
||||||
|
return this.devices[deviceName];
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeDeviceList(devicesToTurnOn: Array<IDevice>, controlUnitName: string): Array<IDevice> {
|
||||||
|
for (let controlUnitKey in this.states) {
|
||||||
|
//Skip self
|
||||||
|
if (controlUnitKey === controlUnitName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let currentOtherState: IActivityState = this.states[controlUnitKey]!;
|
||||||
|
|
||||||
|
if (currentOtherState) {
|
||||||
|
currentOtherState.currentActivity.deviceSetupItems.forEach((value: DeviceSetupItem) => {
|
||||||
|
//there are devices to remove
|
||||||
|
if (devicesToTurnOn.some(e => e.name === value.deviceName)) {
|
||||||
|
let deviceToRemove: IDevice = devicesToTurnOn.filter(i => i.name === value.deviceName)[0];
|
||||||
|
delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devicesToTurnOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendCommand = async (command: string) => {
|
||||||
|
try {
|
||||||
|
let response = await this.harmony.sendCommand(JSON.stringify(command));
|
||||||
|
this.log(`Sent command: ${JSON.stringify(command)} response: ${JSON.stringify(response)}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.log(`ERROR - error sending command to harmony: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HarmonyDataProvider;
|
@ -48,7 +48,7 @@ export class Activity {
|
|||||||
/**
|
/**
|
||||||
* The device associated with output.
|
* The device associated with output.
|
||||||
*/
|
*/
|
||||||
public outputDeviceId(): string {
|
public get outputDeviceId(): string {
|
||||||
return this._outputDeviceId;
|
return this._outputDeviceId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export class Activity {
|
|||||||
return this._displayName;
|
return this._displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get deviceSetupItem(): Array<DeviceSetupItem> {
|
public get deviceSetupItems(): Array<DeviceSetupItem> {
|
||||||
return this._deviceSetupItems
|
return this._deviceSetupItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
export interface IDeviceSetupItemProps {
|
export interface IDeviceSetupItemProps {
|
||||||
deviceId: string,
|
deviceName: string,
|
||||||
input: string
|
input: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8,11 +8,11 @@ export class DeviceSetupItem {
|
|||||||
private _deviceId: string = "";
|
private _deviceId: string = "";
|
||||||
private _input: string = "";
|
private _input: string = "";
|
||||||
constructor(props: IDeviceSetupItemProps) {
|
constructor(props: IDeviceSetupItemProps) {
|
||||||
this._deviceId = props.deviceId;
|
this._deviceId = props.deviceName;
|
||||||
this._input = props.input;
|
this._input = props.input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get deviceId() {
|
public get deviceName() {
|
||||||
return this._deviceId;
|
return this._deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface IMatrixProps {
|
export interface IMatrixProps {
|
||||||
inputs: Array<Input>,
|
inputs: Array<Input>,
|
||||||
outputs: Array<Output>,
|
outputs: Array<Output>,
|
||||||
|
deviceName: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Input {
|
export interface Input {
|
||||||
@ -16,10 +17,12 @@ export interface Output {
|
|||||||
export class Matrix {
|
export class Matrix {
|
||||||
private _inputs: Array<Input> = [];
|
private _inputs: Array<Input> = [];
|
||||||
private _outputs: Array<Output> = [];
|
private _outputs: Array<Output> = [];
|
||||||
|
private _deviceName: string;
|
||||||
|
|
||||||
constructor(props: IMatrixProps) {
|
constructor(props: IMatrixProps) {
|
||||||
this._inputs = props.inputs;
|
this._inputs = props.inputs;
|
||||||
this._outputs = props.outputs;
|
this._outputs = props.outputs;
|
||||||
|
this._deviceName = props.deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get inputs(): Array<Input> {
|
public get inputs(): Array<Input> {
|
||||||
@ -29,4 +32,8 @@ export class Matrix {
|
|||||||
public get outputs(): Array<Output> {
|
public get outputs(): Array<Output> {
|
||||||
return this._outputs;
|
return this._outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get deviceName(): string {
|
||||||
|
return this._deviceName;
|
||||||
|
}
|
||||||
}
|
}
|
18
src/index.ts
18
src/index.ts
@ -2,6 +2,7 @@ import { ControlUnit } from "./Accessories/ControlUnit";
|
|||||||
import { Activity } from "./Models/Activity";
|
import { Activity } from "./Models/Activity";
|
||||||
import { DeviceSetupItem } from "./Models/DeviceSetupItem";
|
import { DeviceSetupItem } from "./Models/DeviceSetupItem";
|
||||||
import { Input, Output, Matrix } from "./Models/Matrix";
|
import { Input, Output, Matrix } from "./Models/Matrix";
|
||||||
|
import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider";
|
||||||
|
|
||||||
export default function (homebridge: any) {
|
export default function (homebridge: any) {
|
||||||
homebridge.registerPlatform(
|
homebridge.registerPlatform(
|
||||||
@ -29,9 +30,14 @@ class HarmonyMatrixPlatform {
|
|||||||
* @param callback
|
* @param callback
|
||||||
*/
|
*/
|
||||||
accessories(callback: (accessories: Array<ControlUnit>) => void) {
|
accessories(callback: (accessories: Array<ControlUnit>) => void) {
|
||||||
|
//Parse ip
|
||||||
|
let hubIp: string = this.config["hubIp"];
|
||||||
|
|
||||||
//Parse matrix
|
//Parse matrix
|
||||||
let configInputs: any = this.config["Matrix"]["Inputs"];
|
let configInputs: any = this.config["Matrix"]["Inputs"];
|
||||||
let configOutputs: any = this.config["Matrix"]["Outputs"];
|
let configOutputs: any = this.config["Matrix"]["Outputs"];
|
||||||
|
let matrixName: string = this.config["Matrix"]["DeviceName"];
|
||||||
|
|
||||||
let inputs: Array<Input> = [];
|
let inputs: Array<Input> = [];
|
||||||
let outputs: Array<Output> = [];
|
let outputs: Array<Output> = [];
|
||||||
|
|
||||||
@ -62,6 +68,14 @@ class HarmonyMatrixPlatform {
|
|||||||
let matrix = new Matrix({
|
let matrix = new Matrix({
|
||||||
inputs: inputs,
|
inputs: inputs,
|
||||||
outputs: outputs,
|
outputs: outputs,
|
||||||
|
deviceName: matrixName,
|
||||||
|
});
|
||||||
|
|
||||||
|
//construct data provider
|
||||||
|
let dataProvider = new HarmonyDataProvider({
|
||||||
|
hubAddress: hubIp,
|
||||||
|
matrix: matrix,
|
||||||
|
log: this.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
//Parse control units
|
//Parse control units
|
||||||
@ -78,7 +92,7 @@ class HarmonyMatrixPlatform {
|
|||||||
configDevices.forEach((configDevice: any) => {
|
configDevices.forEach((configDevice: any) => {
|
||||||
//Add device
|
//Add device
|
||||||
devices.push(new DeviceSetupItem({
|
devices.push(new DeviceSetupItem({
|
||||||
deviceId: configDevice["DeviceName"],
|
deviceName: configDevice["DeviceName"],
|
||||||
input: configDevice["Input"]
|
input: configDevice["Input"]
|
||||||
}));
|
}));
|
||||||
this.log(`INFO - Added device '${configDevice["DeviceName"]}' for activity '${configActivity["DisplayName"]}'`);
|
this.log(`INFO - Added device '${configDevice["DeviceName"]}' for activity '${configActivity["DisplayName"]}'`);
|
||||||
@ -98,11 +112,11 @@ class HarmonyMatrixPlatform {
|
|||||||
|
|
||||||
//Add control unit
|
//Add control unit
|
||||||
controlUnits.push(new ControlUnit({
|
controlUnits.push(new ControlUnit({
|
||||||
|
dataProvider: dataProvider,
|
||||||
displayName: configControlUnit["DisplayName"],
|
displayName: configControlUnit["DisplayName"],
|
||||||
api: this.api,
|
api: this.api,
|
||||||
log: this.log,
|
log: this.log,
|
||||||
activities: activities,
|
activities: activities,
|
||||||
matrix: matrix,
|
|
||||||
}));
|
}));
|
||||||
this.log(`INFO - Added ControlUnit`);
|
this.log(`INFO - Added ControlUnit`);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user